机器学习实战之集体智慧编程学习笔记(2):聚类

    • 聚类的作用
    • 监督学习与无监督学习
      • 监督学习
      • 无监督学习
    • 数据源
    • 聚类分类
    • 分级聚类
    • k-均值聚类
    • 对偏好的聚类
    • 二维聚类
    • 思维导图
    • 由于代码中都有很详细的注解所以没有做过多的解释有问题请留言或私信解决

聚类的作用

通过聚类,我们可以跟踪统计消费者信息,发现具有相似消费习惯的群体,并据此开发相应的产品或者市场策略

监督学习与无监督学习

监督学习

利用样本输入和期望输出来学习如何预测的技术

  • 神经网络
  • 决策树
  • 向量支持机
  • 贝叶斯过滤

无监督学习

无监督学习不是利用样本进行训练,而是要在一组数据中找寻某种结构

  • 聚类
  • 非负矩阵因式分解
  • 自组织映射

数据源

由于本文主要讲述聚类,所以对数据来源不做记录,此处提供本文数据源下载地址

读取数据:

# 读取博客的统计数据
def readFile():
    with open('blogdata.txt', 'r')as f:
        lines = [line for line in f]
        # 第一行数据是列名称,去掉第一个blog字样
        colNames = lines[0].replace('\n', '').split('\t')[1:]
        # 每一行的第一个数据是行名称
        rowNames = []
        # data数据不包含每一行的第一列
        data = []
        for i in range(1, len(lines)):
            # 去除换行符合空格
            l = lines[i].replace('\n', '').split('\t')
            # 行名是第一个数据
            rowNames.append(l[0])
            data.append([float(x) for x in l[1:]])

        return colNames, rowNames, data

聚类分类

  • 分级聚类
  • k-均值聚类
  • 二位空间聚类

分级聚类

通过连续的把最相近的群组合并为新的群组来构造一个全新的群组,每一个群组都是从单一元素开始的,其过程如下图:


在上图中,每次都把最近的两个元素放在一起,合成一个新的元素,然后求得这个元素里两个元素的平均值作为新的值,再重复上面过程直到最后只剩下一个元素就可以了,而所有的元素内容都会保存在我们的数据中
对于数据的紧密度,我们依然采用上一篇提到过的皮尔逊系数:

# 比较数据d1和d2的相似度
def pearson(d1, d2):
    # 求和
    sum1 = sum(d1)
    sum2 = sum(d2)
    # 平方和
    sumSq1 = sum([pow(v, 2) for v in d1])
    sumSq2 = sum([pow(v, 2) for v in d2])

    # 乘积之和
    pSum = sum([d1[i] * d2[i] for i in range(len(d1))])

    num = pSum - (sum1 * sum2 / len(d1))
    den = math.sqrt(((sumSq1 - pow(sum1, 2) / len(d1)) * (sumSq2 - pow(sum2, 2) / len(d2))))

    if den == 0: return 0

    # num/den得到皮尔逊系数,这个数字越大表示两个数据集相似度越高
    # 用1-num/den得到的结果表示两个数据集之间的距离,相似度越高距离越近
    return 1 - num / den

新建一个类作为聚类的载体:

# 用来保存聚合数据的类
# vec 保存聚合数据
# left 是聚合数据的左节点
# right 是聚合数据的右节点
# id 可以用来判断数据是原始数据还是聚合数据,如果是原始数据还可以根据id获取对应的行名称
# distance中保存原始数据的距离
class bicluster:
    def __init__(self, vec, left=None, right=None, id=None, distance=None):
        self.vec = vec
        self.left = left
        self.right = right
        self.id = id
        self.distance = distance

有了上面的载体,我们只需要循环把两个最相近的组聚合,然后重复这个过程,就能得到一个包含了所有数据的最终聚类

# 分级聚类,将数据聚合成一个bicluster对象
def hcluster(data, distance=pearson):
    distances = {}
    currentclustId = -1
    # 原始的聚类就是所有数据的集合
    clust = [bicluster(data[i], id=i) for i in range(len(data))]
    # 大循环
    while len(clust) > 1:

        # 默认0/1是每次大循环开始时最近的数据
        # lowestpair保存最近的一组数据,closest保存他们的距离
        lowestpair = (0, 1)
        closest = distance(clust[0].vec, clust[1].vec)
        # 两次循环保证所有数据可以比较
        for i in range(len(clust)):
            for j in range(len(clust)):
                # 不跟自己比
                if i == j: continue
                # 如果当前数据没有计算过才计算,不直接用i,j是因为聚合之后i,j就不跟原始的数据对应了
                if (clust[i].id, clust[j].id) not in distances:
                    distances[(clust[i].id, clust[j].id)] = distance(clust[i].vec, clust[j].vec)

                d = distances[(clust[i].id, clust[j].id)]
                # 当前的比最近的还近,替换
                if d < closest:
                    # 在这个大循环结束之前,i/j组合还可以代表最近的组
                    lowestpair = (i, j)
                    closest = d

        # 获取当前最近组的所有项的平均值
        mergevec = [(clust[lowestpair[0]].vec[i] + clust[lowestpair[1]].vec[i]) / 2 for i in range(len(data[0]))]
        # 构造新的组,这个组中包含了子数据的所有信息
        newclust = bicluster(mergevec, left=clust[lowestpair[0]], right=clust[lowestpair[1]], id=currentclustId,
                             distance=closest)

        # 清除原始数据组,加入新数据
        currentclustId -= 1
        del clust[lowestpair[1]]
        del clust[lowestpair[0]]
        clust.append(newclust)
    print(clust[0])
    return clust[0]

通过循环执行,得到了唯一的聚类,我们可以来尝试一下使用:

colNames, rowNames, data = readFile()
hcluster(data)

这个过程会消耗一定的时间,结果会打印出来一个bicluster的对象

但是这样一个对象并不能让我们直观的感受到各个数据之间的关系,所以我们需要想办法使聚类的结构关系可视化,此处引入一个很好用的Python图像处理库PythonImagingLibrary,简称PIL,如果对这个库不了解的可以在网上学习一下简单的使用,此处不做过多介绍

要绘制图片,我们需要知道各个元素的高度和图片的宽度,由于线条的长度会根据原始数据的误差进行调整,所以我们还需要计算出总得误差并据此生成一个误差因子:

# 获取聚类的高度
def getHeight(bicluster):
    # 是原始数据,高度为1
    if bicluster.left is None and bicluster.right is None:
        return 1
    # 非原始数据,高度是两个子数据高度之和
    else:
        return getHeight(bicluster.left) + getHeight(bicluster.right)

#获取聚类的误差
def getDepth(bicluster):
    # 原始数据误差为0
    if bicluster.left is None and bicluster.right is None:
        return 0
    # 聚合数据取误差较大者
    else:
        return max(getDepth(bicluster.left),        getDepth(bicluster.right)) + bicluster.distance

这样我们就可以开始绘图了:

# 绘制图片
def drawDendrogram(bicluster, labels, jpge='clusters.jpeg'):
    # 设置宽高数据
    h = getHeight(bicluster) * 20
    w = 1200
    depath = getDepth(bicluster)
    # 宽度固定,所有留一点额外的空间
    scaling = float((w - 150) / depath)

    image = Image.new('RGB', (w, h), (255, 255, 255))
    draw = ImageDraw.Draw(image)

    draw.line((0, h / 2, 10, h / 2), (255, 0, 0))

    print('draw start...')
    drawNode(bicluster, draw, 10, h / 2, scaling, labels)
    image.save(jpge, 'JPEG')

#递归绘制细节
def drawNode(bicluster, draw, x, y, scaling, labels):
    # 原始数据,显示文字即可
    if bicluster.left is None and bicluster.right is None:
        draw.text((x + 5, y - 7), labels[bicluster.id], (0, 0, 0))
    # 聚合数据,根据聚合两个元素的距离来画
    else:
        h1 = getHeight(bicluster.left) * 20
        h2 = getHeight(bicluster.right) * 20
        # 留出两个子元素高度的空隙
        top = y - (h1 + h2) / 2
        bottom = y + (h1 + h2) / 2
        # 画出竖直的线,高度是两个子元素高度的一半
        draw.line((x, top + h1 / 2, x, bottom - h2 / 2), fill=(255, 0, 0))

        # 画出水平的线,宽度是缩放系数X距离
        ll = scaling * bicluster.distance
        draw.line((x, top + h1 / 2, x + ll, top + h1 / 2), fill=(255, 0, 0))
        draw.line((x, bottom - h2 / 2, x + ll, bottom - h2 / 2), fill=(255, 0, 0))

        # 循环,画左右两个子节点
        drawNode(bicluster.left, draw, x + ll, top + h1 / 2, scaling, labels)
        drawNode(bicluster.right, draw, x + ll, bottom - h2 / 2, scaling, labels)

完成上面的代码之后就可以把我们得到的分级聚类结果显示出来了,还不赶紧试试

有时候我们不仅想获得行元素的聚类,也想看一下列元素的聚类结果,对应我们这次操作的数据来看,行元素的聚类可以让我们查看博客之间的相似性,但是对列元素也就是词组的相似性分析有时候对我们也很有意义,所以我们可以对数据做一下矩阵转换,再对变换过的矩阵做相同的操作,得到我们想要的结果:

def translateXY(data):
    result = []
    # 获取列数,用此进行循环,每个新的组包含了原来的一列元素
    for x in range(len(data[0])):
        # x代表第x列,y代表第y行,这样把每一列的元素都取出来形成新的数组
        newrow = [data[y][x] for y in range(len(data))]
        # 这样添加的数据,他的列数和他的索引是相同的
        result.append(newrow)

    return result

一起调用一下看看:

colNames, rowNames, data = readFile()
drawDendrogram(hcluster(data), rowNames)
drawDendrogram(hcluster(translateXY(data)), colNames, jpge='trans_clusters.jpeg')
print('end')

怎么样,同级目录下是不是多出来两张图片,显示了分组的详细情况?

优点:形象,直观

缺点:并没有真正将数据分组,计算量比较大,很耗时

k-均值聚类

k-均值聚类首先确认每一列元素的范围(计算最大值最小值),然后随机生成k个行,这些行里的每一个列元素都在范围之内

之后我们可以根据元素和这k个随机行之间的距离把元素分为k组,得到了k个组之后我们再把这k个组的所有数据取平均值,就得到了新的组,然后以新的组为中心,不断的重复运算直到组不再变化,就得到了k个组,过程如下:

代码实现:

# k均值聚类
def kclust(data, rowNames, distance=pearson, k=5):
    # 存一下列数,经常要用
    col_num = len(data[0])
    # 随机列的数据
    randomrows = []
    # 通过k-v的形式存储每个随机聚点下的子数据
    last_clusts = {}
    new_clusts = {}

    # 循环列,拿到每一列的最大值和最小值
    # for x in range(len(data[0])):
    #     # 最大值
    #     col_max = max([row[x] for row in data])
    #     # 最小值
    #     col_min = min([row[x] for row in data])
    #     # 每一列对应的正好是索引
    #     max_min.append((col_max, col_min))
    # 简写如下:
    # 存储每一列的最大值和最小值
    max_min = [(max([row[x] for row in data]), min([row[x] for row in data])) for x in range(col_num)]

    # 随机k个行数据
    for i in range(k):
        # max_min[j][0]-max_min[j][1]表示取最大值和最小值的差值,在用这个值X随机数,在加上最小值
        # 得到了最大值和最小值之间的一个随机值
        # 把上述过程进行列数个次数,就得到了一个随机行
        random_row = [(random.randint(0, 1) * (max_min[j][0] - max_min[j][1]) + max_min[j][1]) for j in range(col_num)]
        randomrows.append(random_row)

    # 大循环进行到数据不再更改
    while True:
        for i in range(k):
            new_clusts[i] = []

        # 拿每一行去跟随机行比,找到最近的,算进他的组里
        for i in range(len(data)):
            # 默认最近的是第一个随机行
            c_index = 0
            closest = distance(data[i], randomrows[c_index])
            for j in range(1, k):
                d = distance(randomrows[j], data[i])
                # 找到了更近的
                if d < closest:
                    c_index = j
                    closest = d
            # 把数据放入最近的聚点的名下
            new_clusts[c_index].append((rowNames[i], data[i]))
        # 如果重新排之后数据没变化,说明已完成,退出循环
        if last_clusts == new_clusts: break
        # 数据复制,直接=的话会一直相同,用copy复制出来
        last_clusts = new_clusts.copy()

        # randomrows.clear()
        # for k in new_clusts:
        #
        #     # 如果组里没东西,过
        #     if new_clusts[k] is None or len(new_clusts[k]) == 0: continue
        #     # 对于组中的每一列,求平均值,形成一个结果组,放进原来的随机组里
        #     randomrows.append(
        #         [sum([row[x] for row in new_clusts[k]]) / len(new_clusts[k]) for x in range(col_num)]
        #     )
        # 简写如下:
        randomrows = [[sum([row[1][x] for row in new_clusts[k]]) / len(new_clusts[k]) for x in range(col_num)] for k in
                      new_clusts if new_clusts[k] is not None and len(new_clusts[k]) != 0]
    return new_clusts

由于函数使用了随机的中心点作为开始,所以每次聚类的结果都可能不同

对偏好的聚类

皮尔逊系数更适用于统计数据,如果我们采用的是0/1表示的有/无的数据,就需要其他的度量方法,Tanimoto系数可以满足我们的需求

他是通过数据的交集除以并集得到元素的相关度的,结果越大说明元素越相关

def tanimoto(d1, d2):
    # r1/r2表示d1/d2中的非无数据个数,sr表示交集个数,此处我采用0表示没有数据,1表示有
    r1, r2, sr = 0, 0, 0

    for i in range(len(d1)):
        if d1[i] == 1:
            r1 += 1
        if d2[i] == 1:
            r2 += 1
        if d1[i] == d2[i]:
            sr += 1
    # sr/(r1+r2-sr)得到的数据越大说明相似度越高,但是不利于我们看距离,
    # 所以用1-sr/(r1+r2-sr)来表示距离,值越小说明距离越近,相似度越高
    return 1.0-float(sr / (r1 + r2 - sr))

二维聚类

def scaledown(data, distance=pearson, rate=0.01):
    n = len(data)
    # 记录上次的误差值
    last_err = None
    # 记录数据的真实距离,这是我们的目标结果
    realDis = [[distance(data[j], data[i]) for j in range(n)] for i in range(n)]

    # 每一列随机生成一个坐标点,代表这一列的位置
    rpoints = [[random.random(), random.random()] for i in range(n)]
    # 做一个双层数组存储数据信息
    fakeDis = [[[0.0] for j in range(n)] for i in range(n)]
    while True:
        # 求模拟点之间的距离,视为当前距离
        for i in range(n):
            for j in range(n):
                fakeDis[i][j] = math.sqrt(sum([pow(rpoints[j][x] - rpoints[i][x], 2) for x in range(2)]))

        grad = [[0.0, 0.0] for i in range(n)]

        total_err = 0
        for i in range(n):
            for j in range(n):
                if i == j: continue
                # 记录当前两个点的误差值
                err = (fakeDis[i][j] - realDis[i][j]) / realDis[i][j]
                # i来移动,移动的距离是i,j在x/y轴上的差值/当前距离X误差
                grad[i][0] += ((rpoints[i][0] - rpoints[j][0]) / fakeDis[i][j]) * err
                grad[i][1] += ((rpoints[i][1] - rpoints[j][1]) / fakeDis[i][j]) * err

                total_err += abs(err)

        print(total_err)
        # 移动之后如果会更混乱,则停止
        if last_err is not None and total_err >= last_err: break
        last_err = total_err
        # 根据计算结果移动点的位置
        for i in range(n):
            rpoints[i][0] -= grad[i][0] * rate
            rpoints[i][1] -= grad[i][1] * rate

    return rpoints

绘制聚类结果

def drawPoints(points, labels, jpeg='sdc.jpeg'):
    # 白色背景图
    image = Image.new('RGB', (2000, 2000), (255, 255, 255))
    draw = ImageDraw.Draw(image)
    # 取出移动完毕的点,拿到相应的名称显示出来
    for i in range(len(points)):
        x = points[i][0] * 1000
        y = points[i][1] * 1000
        draw.text((x, y), labels[i], fill=(0, 0, 0))
    image.save(jpeg)

调用

colNames, rowNames, data = readFile()
drawPoints(scaledown(data), rowNames)

思维导图

由于代码中都有很详细的注解,所以没有做过多的解释,有问题请留言或私信解决

你可能感兴趣的:(集体智慧编程,机器学习,机器学习,分级聚类,k-均值聚类,二位聚类)