通过DEC深度聚类算法实现汽车产品聚类(MindSpore框架)

1. 导入模块

import os
import time
import numpy as np
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.preprocessing import LabelEncoder
from sklearn import preprocessing
from sklearn.decomposition import PCA
import mindspore as ms
from mindspore import nn
import mindspore.ops as ops
from mindspore import Tensor, Parameter
from mindspore.dataset import vision, transforms
from mindspore.dataset import MnistDataset
import mindspore.context as context
import mindspore.dataset as ds
from mindspore import ops
from download import download
import matplotlib.pyplot as plt
import seaborn as sns

2. 数据预处理 

# 显示中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
# 显示负号
plt.rcParams['axes.unicode_minus'] = False

### 数据加载 ###
car_price = pd.read_csv('CarPrice.csv')

# 气缸数用具体数值替换
car_price['cylindernumber'] = car_price.cylindernumber.replace({'two':2, 'three':3, 'four':4, 'five':5, 'six':6, 'eight':8, 'twelve':12})

# 修正不规则命名
CarBrand = car_price['CarName'].str.split(expand = True)[0]
CarBrand = CarBrand.replace({'porcshce':'porsche','vokswagen':'volkswagen','Nissan':'nissan','maxda':'mazda','vw':'volkswagen','toyouta':'toyota'})
car_price['CarBrand'] = CarBrand

# 车型大小
bins = [min(car_price.carlength)-0.01, 145.67, 169.29, 181.10, 192.91, 200.79, max(car_price.carlength)+0.01]
label = ['A00','A0','A','B','C','D']
CarSize = pd.cut(car_price.carlength, bins, labels = label)
car_price['CarSize'] = CarSize

# 剔除ID属性 
car_df = car_price.drop(['car_ID','CarName'], axis = 1)

# 剔除相关性强的特征
car_df = car_df.drop(['carlength'], axis = 1)

# 类别型特征离散化处理
CarSize = LabelEncoder().fit_transform(car_df['CarSize']) # LabelEncoder对不具实体数值的数据编码
car_df['CarSize'] = CarSize

cate = car_df.select_dtypes(include = 'object').columns
car_df = car_df.join(pd.get_dummies(car_df[cate])).drop(cate, axis = 1)

# 数值型特征标准化处理 
car_df = preprocessing.MinMaxScaler().fit_transform(car_df)

        数据预处理不是本篇博客的重点,故不过多赘述。 

3. 设备设置 

context.set_context(mode = ms.PYNATIVE_MODE, device_target = 'CPU')

         MindSpore框架可以提供动态图和静态图两种支持,静态图推理速度占优,动态图有利于后期调试。

4. 构建自编码器

class AutoEncoder(nn.Cell):
    def __init__(self):
        super(AutoEncoder, self).__init__()
        self.encoder = nn.SequentialCell(
            nn.Dense(69, 50),
            nn.ReLU(),
            nn.Dense(50, 50),
            nn.ReLU(),
            nn.Dense(50, 50),
            nn.ReLU(),
            nn.Dense(50, 500),
            nn.ReLU(),
            nn.Dense(500, 10),
            nn.ReLU()
        )
        self.decoder = nn.SequentialCell(
            nn.Dense(10, 500),
            nn.ReLU(),
            nn.Dense(500, 50),
            nn.ReLU(),
            nn.Dense(50, 50),
            nn.ReLU(),
            nn.Dense(50, 50),
            nn.ReLU(),
            nn.Dense(50, 69),
            nn.ReLU()
        )

        self.model = nn.SequentialCell(self.encoder, self.decoder)

    def encode(self, x):
        return self.encoder(x)
    
    def construct(self, x):
        x = self.model(x)
        return x

5. 构建聚类层

class ClusteringLayer(nn.Cell):
    def __init__(self, n_clusters = 6, hidden = 10, cluster_centers = None, alpha = 1.0):
        super(ClusteringLayer, self).__init__()
        self.n_clusters = n_clusters
        self.alpha = alpha
        self.hidden = hidden

        if cluster_centers is None:
            shape = (self.n_clusters, self.hidden)
            minval = Tensor(-1, ms.float32)
            maxval = Tensor(1, ms.float32)
            initial_cluster_centers = ops.uniform(shape, minval, maxval, dtype = ms.float32)
        else:
            initial_cluster_centers = cluster_centers
        
        self.cluster_centers = Parameter(initial_cluster_centers)

    # 静态图 图构建好 方有输出
    def construct(self, x): # x (205, 10) self.cluster_centers (6, 10) 
        norm_squared = ops.sum((x.unsqueeze(1) - self.cluster_centers) ** 2, 2) 
        power = float(self.alpha + 1) / 2
        numerator = 1.0 / (1.0 + (norm_squared / self.alpha)) ** power # (205, 6)
        denominator = ops.sum(numerator, 1).unsqueeze(1) # (205, 1)
        t_dist = ops.div(numerator, denominator)
        return t_dist

        x是数据样本经编码器降维后得到的低维稠密向量,其可以作为数据样本的特征表示。cluster_centers是x经k-means算法聚类分配后,得到的簇中心。聚类层用来计算软分配。软分配用来衡量数据样本点与各个簇中心之间的相似度。

6. 构造DEC网络模型 

class DEC(nn.Cell):
    def __init__(self, n_clusters = 6, autoencoder = None, hidden = 10, cluster_centers = None, alpha = 1.0):
        super(DEC, self).__init__()
        self.n_clusters = n_clusters
        self.alpha = alpha
        self.hidden = hidden
        self.cluster_centers = cluster_centers
        self.autoencoder = autoencoder
        self.clusteringlayer = ClusteringLayer(self.n_clusters, self.hidden, self.cluster_centers, self.alpha)

    def target_distribution(self, q): # q (205, 6)
        p = (q ** 2) / (ops.sum(q, 0).unsqueeze(0))
        return (p / (ops.sum(p, 1).unsqueeze(1)))
    
    def construct(self, x):
        x = self.autoencoder.encode(x) # (205, 10)
        return self.clusteringlayer(x)

        autoencoder是自编码器。clusteringlayer是聚类层。DEC网络模型选取了自编码器中的encoder部分,加入聚类层,使用KL散度进行迭代训练,最小化软分配与辅助目标分布之间的距离,进一步优化学习聚类分配,更新自编码器的参数权值。函数target_distribution的作用是计算辅助目标分布。 

7. 第一阶段预训练自编码器

### 数据增强 ###
def add_noise(img):
    noise = ops.randn(img.shape) * 2
    noisy_img = img + noise
    return Tensor(noisy_img, dtype = ms.float32)

### 模型保存 ###
def save_checkpoint(model, savepath, is_best):
    if is_best:
        print("=> Saving new checkpoint")
        ms.save_checkpoint(model, savepath)
    else:
        print("=> Validation Accuracy did not improve")

### 第一阶段训练 前向传播函数 ###
def pre_train_forward_fn(data, label):
    output = autoencoder(data)
    loss = pre_train_criterion(output, label)
    return loss

### 第一阶段训练 ###
def pre_train(dataset, num_epochs, savepath, checkpoint, start_epoch):
    dataset = dataset.batch(batch_size = 4)
    # 生成求导函数 用于计算给定函数的前向传播结果和梯度
    pre_train_grad_fn = ms.value_and_grad(pre_train_forward_fn, None, pre_train_optimizer.parameters)
    for epoch in range(start_epoch, num_epochs):
        autoencoder.set_train()
        total_loss = 0.
        for [data] in dataset.create_tuple_iterator():
            noisy_data = add_noise(data) # 数据增强

            loss, grads = pre_train_grad_fn(noisy_data, data)
            total_loss += loss.asnumpy()
            pre_train_optimizer(grads)

        loss_avg = total_loss / dataset.get_dataset_size()
        print('epoch[{}/{}], MSE_loss:{:.4f}'.format(epoch + 1, num_epochs, loss_avg))
        is_best = False
        if loss_avg < checkpoint['best']:
            checkpoint['best'] = loss_avg
            is_best = True
        save_checkpoint(autoencoder, savepath, is_best)

        第一阶段,预训练自编码器,进行参数初始化。在PyTorch中,默认情况下,执行前向传播计算时会记录反向传播所需的梯度信息。在推理阶段,这一操作是冗余的,会额外耗时,因此PyTorch提供了torch.no_grad来取消该过程。而MindSpore只有在调用grad时才会根据正向图结构来构建反向图,前向传播时不会记录任何梯度信息。因此,我们需要调用mindspore.value_and_ grad,获得微分函数,用于计算前向传播结果和梯度。然后,我们可以调用MindSpore框架提供的API实现优化器,完成对网络参数的更新。在预训练结束后,我们将自编码器的网络参数权值保存下来,供第二阶段使用。值得注意的是,在第一阶段预训练时,我们引入了随机噪声进行数据增强,希望能够提升模型的泛化能力和鲁棒性。 

8. 第二阶段训练DEC网络模型 

### 第二阶段训练 前向传播函数 ###
def train_forward_fn(data, label):
    output = dec(data)
    loss = train_criterion(output.log(), label)
    return loss

### 第二阶段训练 ###
def train(car_df, dataset, num_epochs, savepath, checkpoint, start_epoch):
    ### encoder降维 ###
    features = []
    for [data] in dataset.create_tuple_iterator():
        data = Tensor(data.unsqueeze(0), dtype = ms.float32)
        feature = dec.autoencoder.encode(data)
        features.append(feature)
    features = ops.cat(features).asnumpy() # (205, 10)
    ### K - Means ###
    kmeans = KMeans(n_clusters = 6, random_state = 0).fit(features)
    cluster_centers = kmeans.cluster_centers_
    cluster_centers = Tensor(cluster_centers, dtype = ms.float32)
    dec.cluster_centers = Parameter(cluster_centers)
    ### 最小化KL散度 ###
    train_grad_fn = ms.value_and_grad(train_forward_fn, None, train_optimizer.parameters)
    for epoch in range(start_epoch, num_epochs):
        dec.set_train()
        car_df_dataset = Tensor(car_df, dtype = ms.float32)
        output = dec(car_df_dataset)
        target = dec.target_distribution(output)
        loss, grads = train_grad_fn(car_df_dataset, target)
        loss = loss 
        train_optimizer(grads)
        
        print('Epochs: [{}/{}] Loss:{}'.format(epoch, num_epochs, loss))
        is_best = False
        if loss < checkpoint['best']:
            checkpoint['best'] = loss
            is_best = True
        
        save_checkpoint(dec, savepath, is_best)

        第二阶段,训练DEC网络模型。首先,利用第一阶段预训练好的自编码器对数据进行压缩,获得数据的特征表示。然后,运用k-means算法进行聚类分配,得到簇中心。接着,计算出软分配和辅助目标分布。最后,使用KL散度进行迭代训练,最小化软分配与辅助目标分布之间的距离,进一步优化学习聚类分配,更新自编码器的参数权值。第二阶段训练结束后,保存网络参数权值,便于后续使用。 

9.  准备数据集并两阶段训练模型

# 自定义数据类
class dataset_class:
    def __init__(self, datas):
        self.datas = datas

    def __getitem__(self, index):
        return self.datas[index]

    def __len__(self):
        return len(self.datas)
    
### 生成数据集 ###
pre_train_dataset = ds.GeneratorDataset(dataset_class(car_df), ["datas"], shuffle = False) # 迭代对象需要返回numpy array
train_dataset = ds.GeneratorDataset(dataset_class(car_df), ["datas"], shuffle = False)

### 超参数设置 ### 
epochs_pre = 10
epochs_train = 10

checkpoint = {
	"best": float("inf")
}

ae_save_path = 'saves/autoencoder.ckpt'
autoencoder = AutoEncoder()
pre_train_optimizer = nn.Adam(autoencoder.trainable_params(), learning_rate = 1e-3)
pre_train_criterion = nn.MSELoss()
# pre_train(dataset = pre_train_dataset, num_epochs = epochs_pre, savepath = ae_save_path, checkpoint = checkpoint, start_epoch = 0)

### 加载第一阶段训练好的自编码器 ###
param_dict = ms.load_checkpoint("saves/autoencoder.ckpt")
param_not_load, _ = ms.load_param_into_net(autoencoder, param_dict)

dec_save_path = 'saves/dec.ckpt'
dec = DEC(n_clusters = 6, autoencoder = autoencoder, hidden = 10, cluster_centers = None, alpha = 1.0)
train_criterion = nn.KLDivLoss()
train_optimizer = nn.SGD(dec.trainable_params(), learning_rate = 1e-3)
train(car_df = car_df, dataset = train_dataset, num_epochs = epochs_train, savepath = dec_save_path, checkpoint = checkpoint, start_epoch = 0)

        在第一阶段预训练自编码器时,优化器我选择了Adam。Adam在RMSProp的基础上引入了动量,其既可以为不同的参数计算不同的自适应学习率,累计梯度更大的参数更新步长更小,累计梯度更小的参数更新步长更大,同时Adam具有惯性保持的优势,即每次参数更新时,均会考虑过往所有的梯度信息,使得当前用于参数更新的梯度与上一次用于参数更新的梯度相差不会太大,更为平滑,同时参数更新将更加稳定,可以适应不稳定的目标函数。损失函数我选择了均方误差损失。由于自编码器试图重构输入,本质上是回归问题。对于回归问题,我们往往假设预测值属于高斯分布。经最大似然估计,可以推导出均方误差损失。均方误差损失是回归问题中常用的损失函数。在第二阶段训练DEC网络模型,优化器我选择了SGD。第二阶段,我们计算了辅助目标分布,其和软分配一样是衡量数据样本属于某个簇的分布。计算辅助目标分布时,我们需要考虑所有数据样本。因此第二阶段我们将所有数据样本作为DEC网络模型的输入。由于Adam等优化器仅对于mini-batch training有实际意义。当batch size为整个数据集时,Adam等优化器退化为随机梯度下降。选择SGD进行优化,能达到相同效果,同时还能减少参数开销。损失函数我选择了KL散度损失。与原论文一致,希望最小化软分配与辅助目标分布之间的距离。值得注意的是,KL散度损失的预测输入需要进行.log()对数处理,否则计算出的结果为负数。

10. 推理验证 

initail_autoencoder = AutoEncoder()
dec = DEC(n_clusters = 6, autoencoder = initail_autoencoder, hidden = 10, cluster_centers = None, alpha = 1.0)
param_dict = ms.load_checkpoint("saves/dec.ckpt")
param_not_load, _ = ms.load_param_into_net(dec, param_dict)

test_dataset = Tensor(car_df, dtype = ms.float32)
feature = dec.autoencoder.encode(test_dataset)
kmeans = KMeans(n_clusters = 6, init = 'k-means++', max_iter = 300)
y_pred = kmeans.fit_predict(feature)
print('DEC', silhouette_score(feature, y_pred))

11. 总结

        本篇博客详尽地展示了用MindSpore框架实现DEC算法的全流程。

        博主在学习过程中发现,mindspore2.0.0-rc1框架提供了动态图和静态图两种模式。相较于动态图而言,静态图的特点是将计算图的构建和实际计算分开(Define and run)。在构建阶段,根据完整的计算流程对原始的计算图进行优化和调整,编译得到更省内存和计算量更少的计算图。在计算阶段,根据输入数据执行编译好的计算图得到计算结果。相较于动态图,静态图对全局的信息掌握更丰富,可做的优化也会更多,但是其中间过程对于用户来说是黑盒,无法像动态图一样实时拿到中间计算结果。Pytorch框架采用动态图模式。动态图的特点是计算图的构建和计算同时发生(Define by run),其符合Python的解释执行方式。在调试模型时较为方便,能够实时得到中间结果的值,但由于所有节点都需要被保存,导致难以对整个计算图进行优化

        初期,由于对mindspore框架的不熟悉。博主一直在静态图模式下DeBug,时常没有中间输出,影响调试效率。仔细阅读官方文档后,这个问题才得以解决。

        除此之外,在调用mindspore框架提供的API接口ms.value_and_grad,计算模型输出结果和模型参数梯度时。尽管官文文档显示ms.value_and_grad可以传入网络模型,也可以传入前向传播函数。但经超级多次实验测试之后,博主发现传入网络模型时,ms.value_and_grad只能计算模型输入的梯度,而无法计算模型参数的梯度。传入前向传播函数时,前向传播函数中必须包括模型推理和损失计算两个过程,二者缺一不可!而且,当前向传播函数返回损失值,模型输出等多个数据时损失值必须在return的首位!否则均会出现模型参数值停止更新,模型损失停止波动的现象。

你可能感兴趣的:(算法,聚类)