Machine Learning in Action 读书笔记---第10章 利用k-均值聚类算法对未标注数据分组

本文介绍了如何使用k-均值算法对地图上的点进行聚类,展示了如何通过实例分析来提高聚类性能,包括后处理策略、二分K-均值算法的应用,以及如何通过SSE评价聚类效果。通过具体步骤和示例,读者能理解无监督学习中的K-means在实际问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Machine Learning in Action 读书笔记

第10章 利用k-均值聚类算法对未标注数据分组



一、聚类

    前面学到的监督学习可以大致分为两类:分类回归。本章开始学习无监督学习,所谓的无监督学习是指事先不知道要寻找的内容,即没有目标变量。
    聚类(Clustering)是一种无监督的学习方法,聚类将数据点归到多个簇中,其中相似数据点处于同一簇,而不相似数据点处于不同簇中。本章要学习的一种称为k-均值(K-means)聚类的算法。k-均值聚类算法可以发现k个不同的簇,且每个簇的中心采用簇中所含值的均值计算而成。
    簇识别(cluster identification) 可以给出聚类结果的含义。

聚类和分类的最大不同在于,分类的目标事先已知,而聚类则未知,因此,聚类有时也被称为无监督分类(unsupervised classification)

二、K-均值聚类算法

  • 优点:容易实现
  • 缺点:可能收敛到局部最小值,在大规模数据集上收敛较慢
  • 适用数据类型:数值型数据

k-均值是发现给定数据集的k个簇的算法,k由用户给定,每一个簇通过其质心(centroid),即簇中所有点的中心来描述。

1.K-均值算法的工作流程

  1. 随机确定k个初始点作为质心
  2. 将数据集中的每个点分配到一个簇中,即为每个点找距其最近的质心,并将其分配该质心对应的簇
  3. 每个簇的质心更新为该簇所有点的平均值

2.K-均值聚类的一般流程

  1. 收集数据:适用任意方法
  2. 准备数据:需要数值型数据来计算距离,也可以将标称型数据映射为二值型数据再用于距离计算
  3. 分析数据:使用任意方法
  4. 训练数据:不适应于无监督学习,即无监督学习没有训练过程
  5. 测试数据:应用聚类算法、观察结果。可以使用量化的误差指标如误差平方和来评价算法的结果
  6. 使用算法:可以用于所希望的任何应用。通常情况下,簇质心可以代表整个簇的数据来做出决策。
'''k-均值聚类算法的支持(辅助)函数'''
def loadDataSet(fileName):
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = list(map(float, curLine))
        dataMat.append(fltLine)
    return dataMat

# distEclud函数用于计算两个向量的欧式距离
def distEclud(vecA, vecB):
    return sqrt(sum(power(vecA - vecB, 2)))

# randCent为给定数据集构建一个包含k个随机质心的集合(随机质心必须要在整个数据集的边界之内)
def randCent(dataSet, k): # k为设置质心的数目
    n = shape(dataSet)[1]  # 2
    centroids = mat(zeros((k, n)))# 构建一个质心矩阵 4*2
    for j in range(n):#创建随机聚类质心
        minJ = min(dataSet[:,j])
        rangeJ = float(max(dataSet[:,j]) - minJ)
        centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1)) # random.rand(k,1)创建一个k行1列的矩阵,值在0到1之间
    return centroids

'''k-均值聚类算法'''
# 计算质心-分配-重新计算质心(迭代执行)
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent): # k表示簇的数目; 四个参数中数据集和k是必选参数,用来计算距离和创建初始质心 的函数是可选的
    m = shape(dataSet)[0]  # 输出数据集中数据点的总数
    clusterAssment = mat(zeros((m,2)))#创建一个矩阵,用于存储每个点的簇分配结果,包含两列:第一列记录簇索引值,第二列存储误差(误差是指当前点到簇质心的距离,后边使用该误差来评价聚类的效果)
    centroids = createCent(dataSet, k)
    clusterChanged = True  # 该值为true,就继续迭代,该值为false表示所有数据点的簇分配结果不再改变,停止迭代
    while clusterChanged:
        clusterChanged = False
        for i in range(m):#为每个数据点分配最近质心
            minDist = inf; minIndex = -1
            for j in range(k):  # 寻找最近的质心:对每个点遍历所有质心,后计算点到每个质心的距离
                distJI = distMeas(centroids[j,:],dataSet[i,:]) # 计算距离,默认距离函数是distEclud()
                if distJI < minDist:
                    minDist = distJI; minIndex = j
            if clusterAssment[i,0] != minIndex: clusterChanged = True # 如果任一点的簇分配结果发生改变,则更新clusterChanged标志
            clusterAssment[i,:] = minIndex,minDist**2
        # print(centroids)
        for cent in range(k):# 遍历所有质心并更新它们的取值
            ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]#通过数组过滤来获得给定簇的所有点
            centroids[cent,:] = mean(ptsInClust, axis=0) #计算所有点的均值,axis=0表示沿矩阵的列方向进行均值计算
    return centroids, clusterAssment  # 返回所有的类质心与点分配结果

三、使用后处理来提高聚类性能

    在上面k-均值聚类算法中,返回值包含两部分,质心和簇的分配结果,其中簇的分配结果有两列,第一列是该数据点所属的簇,第二列为该数据点到质心的距离平方值,即每个点的误差。利用该误差可以来评价聚类质量,即k值选择的是否合适或者簇分的是否好。
    K-均值算法收敛但聚类效果较差的原因是:K-均值算法收敛到了局部最小值,而非全局最小值(局部最小值指结果还可以但并非最好结果,全局最小值是可能的最好结果)。
    一种用于度量聚类效果的指标是SSE(sum of squared error,误差平方和)。SSE值越小表示数据点越接近于它们的质心,聚类效果也越好。
    一种肯定可以降低SSE值的方法是增加簇的个数,但这违背了聚类的目标。聚类的目标是在保持簇数目不变的情况下提高簇的质量。


可以通过对生成的簇进行后处理来改进,后处理的方法有:

  • 将具有最大SSE值的簇划分成两个簇。

遇到高维数据,有两种量化方法:

  • 合并最近质心
  • 合并两个使得SSE增幅最小的质心

四、二分K-均值算法

二分K-均值算法可以克服K-均值算法收敛于局部最小值的问题。
伪代码如下:

将所有点看成一个簇
当簇数目小于k时
	对于每个簇
		计算总误差
		在给定的簇上面进行K-均值聚类(k=2)
		计算将该簇一分为二之后的总误差
	选择使得误差最小的那个簇进行划分操作
'''二分k-均值聚类算法'''
def biKmeans(dataSet, k, distMeas=distEclud): # 给定的数据集、所期望的簇数目、距离计算方法
    m = shape(dataSet)[0] # 数据点的数目
    clusterAssment = mat(zeros((m,2))) # 使用矩阵存储数据集中每个点的簇分配结果及平方误差
    centroid0 = mean(dataSet, axis=0).tolist()[0]# 计算整个数据集的质心
    centList =[centroid0] #使用列表来保存所有质心
    for j in range(m):#计算每个点到质心的误差值
        clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
    while (len(centList) < k):  # while循环会不停的对簇进行划分,直到得到想要的簇数目为止
        lowestSSE = inf  # SSE,sum of squared error,误差平方和,用于度量聚类效果的指标
        for i in range(len(centList)):
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]#获得聚类中的当前数据点
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) # 对当前数据点进行二分类
            sseSplit = sum(splitClustAss[:,1])#计算总误差
            sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1]) # 计算剩余数据集的误差
            print("sseSplit, and notSplit: ",sseSplit,sseNotSplit)
            if (sseSplit + sseNotSplit) < lowestSSE:  # 使用分类后的误差和剩余数据集的误差和作为本次划分的误差,与最小误差进行比较
                bestCentToSplit = i
                bestNewCents = centroidMat
                bestClustAss = splitClustAss.copy()
                lowestSSE = sseSplit + sseNotSplit # 使用分类后的误差和剩余数据集的误差和作为本次划分的误差
        bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #通过两个数组过滤器将这些簇编号需改为划分簇及新加簇的编号
        bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit
        print('the bestCentToSplit is: ',bestCentToSplit)
        print('the len of bestClustAss is: ', len(bestClustAss))
        centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]#用两个最好的质心代替当前质心
        centList.append(bestNewCents[1,:].tolist()[0])
        clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss#重新分配新的簇和误差
    return mat(centList), clusterAssment

五、示例:对地图上的点进行聚类

获得数据places.txt:

'''示例:对地图上的点进行聚类'''
import urllib
import urllib.request
import json
def geoGrab(stAddress, city): # 街道地址和城市
    apiStem = 'http://where.yahooapis.com/geocode?'  #create a dict and constants for the goecoder
    params = {}
    params['flags'] = 'J'#JSON return type
    params['appid'] = 'aaa0VN6k'
    params['location'] = '%s %s' % (stAddress, city)
    url_params = urllib.parse.urlencode(params) #urlencode函数将创建的字典转换为可以通过URL进行传递的字符串格式
    yahooApi = apiStem + url_params      #print url_params
    # print(yahooApi) #http://where.yahooapis.com/geocode?flags=J&appid=aaa0VN6k&location=1+VA+Center+Augusta%2C+ME
    c=urllib.request.urlopen(yahooApi)
    return json.loads(c.read()) # 到这里也就意味着成功的对一个地址进行了地理编码

from time import sleep
def massPlaceFind(fileName): # 为了生成文件places.txt
    fw = open('places.txt', 'w')
    for line in open(fileName).readlines():
        line = line.strip()
        lineArr = line.split('\t')
        retDict = geoGrab(lineArr[1], lineArr[2])
        if retDict['ResultSet']['Error'] == 0:
            lat = float(retDict['ResultSet']['Results'][0]['latitude'])
            lng = float(retDict['ResultSet']['Results'][0]['longitude'])
            print("%s\t%f\t%f" % (lineArr[0], lat, lng))
            fw.write('%s\t%f\t%f\n' % (line, lat, lng))
        else: print("error fetching")
        sleep(1) # 为了确保在短时间内过于频繁地调用API
    fw.close()

对地理坐标进行聚类,并绘图:

'''球面距离计算'''
def distSLC(vecA, vecB):#返回地球表面两点之间的距离,单位是英里
    a = sin(vecA[0,1]*pi/180) * sin(vecB[0,1]*pi/180)
    b = cos(vecA[0,1]*pi/180) * cos(vecB[0,1]*pi/180) * \
                      cos(pi * (vecB[0,0]-vecA[0,0]) /180)
    return arccos(a + b)*6371.0 # 球面余弦定理

'''簇绘图函数'''
import matplotlib
import matplotlib.pyplot as plt
def clusterClubs(numClust=5): # 将文本文件中的俱乐部进行聚类并画出结果,参数为所希望得到的簇数目
    datList = []
    for line in open('places.txt').readlines():
        lineArr = line.split('\t')
        datList.append([float(lineArr[4]), float(lineArr[3])])
    datMat = mat(datList)
    myCentroids, clustAssing = biKmeans(datMat, numClust, distMeas=distSLC) # 聚类
    fig = plt.figure()
    rect=[0.1,0.1,0.8,0.8]
    scatterMarkers=['s', 'o', '^', '8', 'p', \
                    'd', 'v', 'h', '>', '<'] # 构建一个标记形状的列表,用于绘制散点图
    axprops = dict(xticks=[], yticks=[])
    ax0=fig.add_axes(rect, label='ax0', **axprops)
    imgP = plt.imread('Portland.png') # 使用imread()函数基于一副图像来创建矩阵
    ax0.imshow(imgP) # 使用imshow()函数绘制上面矩阵
    ax1=fig.add_axes(rect, label='ax1', frameon=False) # 在同一幅图上绘制一张新的图,允许使用两套坐标系统并且不做任何缩放或偏移
    for i in range(numClust): # 遍历每一个簇,并将它们一一画出来
        ptsInCurrCluster = datMat[nonzero(clustAssing[:,0].A==i)[0],:]
        markerStyle = scatterMarkers[i % len(scatterMarkers)] # 标记类型从上面创建的标记列表中获得,使用索引i % len(scatterMarkers)来选择标记形状
        ax1.scatter(ptsInCurrCluster[:,0].flatten().A[0], ptsInCurrCluster[:,1].flatten().A[0], marker=markerStyle, s=90)
    ax1.scatter(myCentroids[:,0].flatten().A[0], myCentroids[:,1].flatten().A[0], marker='+', s=300) # 使用十字标记来表示簇中心并在图中显示
    plt.show()

当k = 4:
在这里插入图片描述

当k = 5:
在这里插入图片描述
当k = 6:
在这里插入图片描述
可以看出k=5时效果会好一些。

六、全部代码

from numpy import *

'''k-均值聚类算法的支持(辅助)函数'''
def loadDataSet(fileName):
    dataMat = []
    fr = open(fileName)
    for line in fr.readlines():
        curLine = line.strip().split('\t')
        fltLine = list(map(float, curLine))
        dataMat.append(fltLine)
    return dataMat

# distEclud函数用于计算两个向量的欧式距离
def distEclud(vecA, vecB):
    return sqrt(sum(power(vecA - vecB, 2)))

# randCent为给定数据集构建一个包含k个随机质心的集合(随机质心必须要在整个数据集的边界之内)
def randCent(dataSet, k): # k为设置质心的数目
    n = shape(dataSet)[1]  # 2
    centroids = mat(zeros((k, n)))# 构建一个质心矩阵 4*2
    for j in range(n):#创建随机聚类质心
        minJ = min(dataSet[:,j])
        rangeJ = float(max(dataSet[:,j]) - minJ)
        centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1)) # random.rand(k,1)创建一个k行1列的矩阵,值在0到1之间
    return centroids

'''k-均值聚类算法'''
# 计算质心-分配-重新计算质心(迭代执行)
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent): # k表示簇的数目; 四个参数中数据集和k是必选参数,用来计算距离和创建初始质心 的函数是可选的
    m = shape(dataSet)[0]  # 输出数据集中数据点的总数
    clusterAssment = mat(zeros((m,2)))#创建一个矩阵,用于存储每个点的簇分配结果,包含两列:第一列记录簇索引值,第二列存储误差(误差是指当前点到簇质心的距离,后边使用该误差来评价聚类的效果)
    centroids = createCent(dataSet, k)
    clusterChanged = True  # 该值为true,就继续迭代,该值为false表示所有数据点的簇分配结果不再改变,停止迭代
    while clusterChanged:
        clusterChanged = False
        for i in range(m):#为每个数据点分配最近质心
            minDist = inf; minIndex = -1
            for j in range(k):  # 寻找最近的质心:对每个点遍历所有质心,后计算点到每个质心的距离
                distJI = distMeas(centroids[j,:],dataSet[i,:]) # 计算距离,默认距离函数是distEclud()
                if distJI < minDist:
                    minDist = distJI; minIndex = j
            if clusterAssment[i,0] != minIndex: clusterChanged = True # 如果任一点的簇分配结果发生改变,则更新clusterChanged标志
            clusterAssment[i,:] = minIndex,minDist**2
        # print(centroids)
        for cent in range(k):# 遍历所有质心并更新它们的取值
            ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]#通过数组过滤来获得给定簇的所有点
            centroids[cent,:] = mean(ptsInClust, axis=0) #计算所有点的均值,axis=0表示沿矩阵的列方向进行均值计算
    return centroids, clusterAssment  # 返回所有的类质心与点分配结果

'''二分k-均值聚类算法'''
def biKmeans(dataSet, k, distMeas=distEclud): # 给定的数据集、所期望的簇数目、距离计算方法
    m = shape(dataSet)[0] # 数据点的数目
    clusterAssment = mat(zeros((m,2))) # 使用矩阵存储数据集中每个点的簇分配结果及平方误差
    centroid0 = mean(dataSet, axis=0).tolist()[0]# 计算整个数据集的质心
    centList =[centroid0] #使用列表来保存所有质心
    for j in range(m):#计算每个点到质心的误差值
        clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2
    while (len(centList) < k):  # while循环会不停的对簇进行划分,直到得到想要的簇数目为止
        lowestSSE = inf  # SSE,sum of squared error,误差平方和,用于度量聚类效果的指标
        for i in range(len(centList)):
            ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]#获得聚类中的当前数据点
            centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas) # 对当前数据点进行二分类
            sseSplit = sum(splitClustAss[:,1])#计算总误差
            sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1]) # 计算剩余数据集的误差
            print("sseSplit, and notSplit: ",sseSplit,sseNotSplit)
            if (sseSplit + sseNotSplit) < lowestSSE:  # 使用分类后的误差和剩余数据集的误差和作为本次划分的误差,与最小误差进行比较
                bestCentToSplit = i
                bestNewCents = centroidMat
                bestClustAss = splitClustAss.copy()
                lowestSSE = sseSplit + sseNotSplit # 使用分类后的误差和剩余数据集的误差和作为本次划分的误差
        bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #通过两个数组过滤器将这些簇编号需改为划分簇及新加簇的编号
        bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit
        print('the bestCentToSplit is: ',bestCentToSplit)
        print('the len of bestClustAss is: ', len(bestClustAss))
        centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]#用两个最好的质心代替当前质心
        centList.append(bestNewCents[1,:].tolist()[0])
        clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss#重新分配新的簇和误差
    return mat(centList), clusterAssment

'''示例:对地图上的点进行聚类'''
import urllib
import urllib.request
import json
def geoGrab(stAddress, city): # 街道地址和城市
    apiStem = 'http://where.yahooapis.com/geocode?'  #create a dict and constants for the goecoder
    params = {}
    params['flags'] = 'J'#JSON return type
    params['appid'] = 'aaa0VN6k'
    params['location'] = '%s %s' % (stAddress, city)
    url_params = urllib.parse.urlencode(params) #urlencode函数将创建的字典转换为可以通过URL进行传递的字符串格式
    yahooApi = apiStem + url_params      #print url_params
    # print(yahooApi) #http://where.yahooapis.com/geocode?flags=J&appid=aaa0VN6k&location=1+VA+Center+Augusta%2C+ME
    c=urllib.request.urlopen(yahooApi)
    return json.loads(c.read()) # 到这里也就意味着成功的对一个地址进行了地理编码

from time import sleep
def massPlaceFind(fileName): # 为了生成文件places.txt
    fw = open('places.txt', 'w')
    for line in open(fileName).readlines():
        line = line.strip()
        lineArr = line.split('\t')
        retDict = geoGrab(lineArr[1], lineArr[2])
        if retDict['ResultSet']['Error'] == 0:
            lat = float(retDict['ResultSet']['Results'][0]['latitude'])
            lng = float(retDict['ResultSet']['Results'][0]['longitude'])
            print("%s\t%f\t%f" % (lineArr[0], lat, lng))
            fw.write('%s\t%f\t%f\n' % (line, lat, lng))
        else: print("error fetching")
        sleep(1) # 为了确保在短时间内过于频繁地调用API
    fw.close()

'''球面距离计算'''
def distSLC(vecA, vecB):#返回地球表面两点之间的距离,单位是英里
    a = sin(vecA[0,1]*pi/180) * sin(vecB[0,1]*pi/180)
    b = cos(vecA[0,1]*pi/180) * cos(vecB[0,1]*pi/180) * \
                      cos(pi * (vecB[0,0]-vecA[0,0]) /180)
    return arccos(a + b)*6371.0 # 球面余弦定理

'''簇绘图函数'''
import matplotlib
import matplotlib.pyplot as plt
def clusterClubs(numClust=5): # 将文本文件中的俱乐部进行聚类并画出结果,参数为所希望得到的簇数目
    datList = []
    for line in open('places.txt').readlines():
        lineArr = line.split('\t')
        datList.append([float(lineArr[4]), float(lineArr[3])])
    datMat = mat(datList)
    myCentroids, clustAssing = biKmeans(datMat, numClust, distMeas=distSLC) # 聚类
    fig = plt.figure()
    rect=[0.1,0.1,0.8,0.8]
    scatterMarkers=['s', 'o', '^', '8', 'p', \
                    'd', 'v', 'h', '>', '<'] # 构建一个标记形状的列表,用于绘制散点图
    axprops = dict(xticks=[], yticks=[])
    ax0=fig.add_axes(rect, label='ax0', **axprops)
    imgP = plt.imread('Portland.png') # 使用imread()函数基于一副图像来创建矩阵
    ax0.imshow(imgP) # 使用imshow()函数绘制上面矩阵
    ax1=fig.add_axes(rect, label='ax1', frameon=False) # 在同一幅图上绘制一张新的图,允许使用两套坐标系统并且不做任何缩放或偏移
    for i in range(numClust): # 遍历每一个簇,并将它们一一画出来
        ptsInCurrCluster = datMat[nonzero(clustAssing[:,0].A==i)[0],:]
        markerStyle = scatterMarkers[i % len(scatterMarkers)] # 标记类型从上面创建的标记列表中获得,使用索引i % len(scatterMarkers)来选择标记形状
        ax1.scatter(ptsInCurrCluster[:,0].flatten().A[0], ptsInCurrCluster[:,1].flatten().A[0], marker=markerStyle, s=90)
    ax1.scatter(myCentroids[:,0].flatten().A[0], myCentroids[:,1].flatten().A[0], marker='+', s=300) # 使用十字标记来表示簇中心并在图中显示
    plt.show()

if __name__ == '__main__':
    datMat = mat(loadDataSet('testSet.txt'))
    # print(datMat)
    # print(min(datMat[:, 0])) #[[-5.379713]]
    # print(min(datMat[:, 1])) #[[-4.232586]]
    # print(max(datMat[:, 0])) #[[4.838138]]
    # print(max(datMat[:, 1])) #[[5.1904]]

    randNum = randCent(datMat, 2)
    # print(randNum) #[[-1.9276751   1.5013302 ]
                   #[ 3.67862177 -3.75580585]]
    dis = distEclud(datMat[0], datMat[1])
    # print(dis) #5.184632816681332

    myCentroids, clustAssing = kMeans(datMat, 4)
    # print(myCentroids)  # 所有的质心
    # print('==============')
    # print(clustAssing) # 点分配结果,第一列为簇索引值(0-3),第二列为点到质心的误差

    datMat3 = mat(loadDataSet('testSet2.txt'))
    # centList, myNewAssments = biKmeans(datMat3, 3)
    # print(centList)

    # geoResults = geoGrab('1 VA Center', 'Augusta, ME')
    # print(geoResults)

    clusterClubs(6)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值