k-means

k-means算法算是经典的机器学习算法,记得应该是大三的课上学过,后面就一直没在接触过该算法对应的问题,现在就来回顾记录一下kmeans算法。

K-means 有一个著名的解释:牧师—村民模型:

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

我们可以看到该牧师的目的是为了让每个村民到其最近中心点的距离和最小。

所以 K-means 的算法步骤为:

  1. 选择初始化的\( \mathrm{k} \)个样本作为初始聚类中心 \(a=a_{1}, a_{2}, \ldots a_{k}\) ;
  2. 针对数据集中每个样本 \(x_{i}\) 计算它到 \(\mathrm{k}\) 个聚类中心的距离并将其分到距离最小的聚类中心所对 应的类中;
  3. 针对每个类别 \(a_{j}\) ,重新计算它的聚类中心 \(a_{j}=\frac{1}{\left|c_{i}\right|} \sum_{x \in c_{i}} x\) (即属于该类的所有样本的质心);
  4. 重复上面 23 两步操作,直到达到某个中止条件(迭代次数、最小误差变化等)。

K-means算法Python实现代码如下:

# -*- coding:utf-8 -*-
import numpy as np
from matplotlib import pyplot


class K_Means(object):
    # k是分组数;tolerance‘中心点误差’;max_iter是迭代次数
    def __init__(self, k=2, tolerance=0.0001, max_iter=300):
        self.k_ = k
        self.tolerance_ = tolerance
        self.max_iter_ = max_iter

    def fit(self, data):
        self.centers_ = {}
        for i in range(self.k_):
            self.centers_[i] = data[i]

        for i in range(self.max_iter_):
            self.clf_ = {}
            for i in range(self.k_):
                self.clf_[i] = []
            # print("质点:",self.centers_)
            for feature in data:
                # distances = [np.linalg.norm(feature-self.centers[center]) for center in self.centers]
                distances = []
                for center in self.centers_:
                    # 欧拉距离
                    # np.sqrt(np.sum((features-self.centers_[center])**2))
                    distances.append(np.linalg.norm(feature - self.centers_[center]))
                classification = distances.index(min(distances))
                self.clf_[classification].append(feature)

            # print("分组情况:",self.clf_)
            prev_centers = dict(self.centers_)
            for c in self.clf_:
                self.centers_[c] = np.average(self.clf_[c], axis=0)

            # '中心点'是否在误差范围
            optimized = True
            for center in self.centers_:
                org_centers = prev_centers[center]
                cur_centers = self.centers_[center]
                if np.sum((cur_centers - org_centers) / org_centers * 100.0) > self.tolerance_:
                    optimized = False
            if optimized:
                break

    def predict(self, p_data):
        distances = [np.linalg.norm(p_data - self.centers_[center]) for center in self.centers_]
        index = distances.index(min(distances))
        return index


if __name__ == '__main__':
    x = np.array([[1, 2], [1.5, 1.8], [5, 8], [8, 8], [1, 0.6], [9, 11]])
    k_means = K_Means(k=2)
    k_means.fit(x)
    print(k_means.centers_)
    for center in k_means.centers_:
        pyplot.scatter(k_means.centers_[center][0], k_means.centers_[center][1], marker='*', s=150)

    for cat in k_means.clf_:
        for point in k_means.clf_[cat]:
            pyplot.scatter(point[0], point[1], c=('r' if cat == 0 else 'b'))

    predict = [[2, 1], [6, 9]]
    for feature in predict:
        cat = k_means.predict(predict)
        pyplot.scatter(feature[0], feature[1], c=('r' if cat == 0 else 'b'), marker='x')

    pyplot.show()

2.1 优点

  • 容易理解,聚类效果不错,虽然是局部最优, 但往往局部最优就够了;
  • 处理大数据集的时候,该算法可以保证较好的伸缩性;
  • 当簇近似高斯分布的时候,效果非常不错;
  • 算法复杂度低。

2.2 缺点

  • K 值需要人为设定,不同 K 值得到的结果不一样;
  • 对初始的簇中心敏感,不同选取方式会得到不同结果;
  • 对异常值敏感;
  • 样本只能归为一类,不适合多分类任务;
  • 不适合太离散的分类、样本类别不平衡的分类、非凸形状的分类。

针对 K-means 算法的缺点,我们可以有很多种调优方式:如数据预处理(去除异常点),合理选择 K 值,高维映射等。

数据预处理

K-means 的本质是基于欧式距离的数据划分算法,均值和方差大的维度将对数据的聚类产生决定性影响。所以未做归一化处理和统一单位的数据是无法直接参与运算和比较的。常见的数据预处理方式有:数据归一化,数据标准化。

此外,离群点或者噪声数据会对均值产生较大的影响,导致中心偏移,因此我们还需要对数据进行异常点检测。

合理选择 K 值

K 值的选取对 K-means 影响很大,这也是 K-means 最大的缺点,常见的选取 K 值的方法有:手肘法、Gap statistic 方法。

手肘法 (Elbow Method) 本质上也是一种间接的观察法。当我们对数据 \(\left\{x_{1}, \ldots, x_{N}\right\}\) 进行K均值聚 类后,我们将得到 \(K\) 个聚类的中心点 \(\mu_{k}, k=1,2, \ldots, K\) ,以及数据点 \(x_{i}\) 所对应的簇 \(C_{k}, k=1,2, \ldots, K\) ,每个簇中有 \(n_{k}\) 个数据点。我们定义每个簇中的所有点相互之间的距离的和 为

$$D_{k}=\sum_{x_{i} \in C_{k}} \sum_{x_{j} \in C_{k}}\left\|x_{i}-x_{j}\right\|^{2}$$

其中 \(\|\cdot\|\) 为2范数。则对于聚类个数为 \(K\) 时,我们可以定义一个测度量为

$$W_{K}=\sum_{k=1}^{K} \frac{1}{2 n_{k}} D_{k}$$

对于不同的 \(K\) ,经过 \(\mathrm{K}-\mathrm{means}\) 算法后我们会得到不同的中心点和数据点所属的簇,从而得到不同的 度量 \(W_{K}\) 。将聚类个数 \(K\) 作为横坐标, \(W_{K}\) 作为纵坐标,我们可以得到类似下面的图像:

图像中图形很像人的手肘,该方法的命名就是从这而来。从图像中我们可以观察到,K=1 到 4 下降很快,K=4 之后趋于平稳,因此 K=4 是一个拐点,手肘法认为这个拐点就是最佳的聚类个数 K。

 Gap statistic 方法,这个方法出自斯坦福大学的几个学者的论文:Estimating the number of clusters in a data set via the gap statistic

$$
\operatorname{Gap}(K)=\mathrm{E}\left(\log D_{k}\right)-\log D_{k}
$$
其中 \(D_{k}\) 为损失函数,这里\(E\left(\log D_{k}\right)\) 指的是 \(\log D_{k}\) 的期望。这个数值通常通过蒙特卡洛 模拟产生,我们在样本里所在的区域中按照均匀分布随机产生和原始样本数一样多的随机样本,并 对这个随机样本做 K-Means,从而得到一个 \(D_{k}\) 。如此往复多次,通常 20 次,我们可以得到 20 个 \(\log D_{k}\)。对这 20 个数值求平均值,就得到了 \(E\left(\log D_{k}\right)\) 的近似值。最终可以计算 Gap Statisitc。而 Gap statistic 取得最大值所对应的 K 就是最佳的 K。

python实现gap:

import numpy as np


def calculate_Wk(data, centroids, cluster):
    K = centroids.shape[0]
    wk = 0.0
    for k in range(K):
        data_in_cluster = data[cluster == k, :]
        center = centroids[k, :]
        num_points = data_in_cluster.shape[0]
        for i in range(num_points):
            wk = wk + np.linalg.norm(data_in_cluster[i, :]-center, ord=2) ** 2

    return wk


def bounding_box(data):
    dim = data.shape[1]
    boxes = []
    for i in range(dim):
        data_min = np.amin(data[:, i])
        data_max = np.amax(data[:, i])
        boxes.append((data_min, data_max))

    return boxes


def gap_statistic(data, max_K, B, cluster_algorithm):
    num_points, dim = data.shape
    K_range = np.arange(1, max_K, dtype=int)
    num_K = len(K_range)
    boxes = bounding_box(data)
    data_generate = np.zeros((num_points, dim))

    ''' 写法1
    log_Wks = np.zeros(num_K)
    gaps = np.zeros(num_K)
    sks = np.zeros(num_K)
    for ind_K, K in enumerate(K_range):
        cluster_centers, labels, _ = cluster_algorithm(data, K)
        log_Wks[ind_K] = np.log(calculate_Wk(data, cluster_centers, labels))

        # generate B reference data sets
        log_Wkbs = np.zeros(B)
        for b in range(B):
            for i in range(num_points):
                for j in range(dim):
                    data_generate[i][j] = \
                        np.random.uniform(boxes[j][0], boxes[j][1])
            cluster_centers, labels, _ = cluster_algorithm(data_generate, K)
            log_Wkbs[b] = \
                np.log(calculate_Wk(data_generate, cluster_centers, labels))
        gaps[ind_K] = np.mean(log_Wkbs) - log_Wks[ind_K]
        sks[ind_K] = np.std(log_Wkbs) * np.sqrt(1 + 1.0 / B)
    '''

    ''' 写法2
    '''
    log_Wks = np.zeros(num_K)
    for indK, K in enumerate(K_range):
        cluster_centers, labels, _ = cluster_algorithm(data, K)
        log_Wks[indK] = np.log(calculate_Wk(data, cluster_centers, labels))

    gaps = np.zeros(num_K)
    sks = np.zeros(num_K)
    log_Wkbs = np.zeros((B, num_K))

    # generate B reference data sets
    for b in range(B):
        for i in range(num_points):
            for j in range(dim):
                data_generate[i, j] = \
                    np.random.uniform(boxes[j][0], boxes[j][1])
        for indK, K in enumerate(K_range):
            cluster_centers, labels, _ = cluster_algorithm(data_generate, K)
            log_Wkbs[b, indK] = \
                np.log(calculate_Wk(data_generate, cluster_centers, labels))

    for k in range(num_K):
        gaps[k] = np.mean(log_Wkbs[:, k]) - log_Wks[k]
        sks[k] = np.std(log_Wkbs[:, k]) * np.sqrt(1 + 1.0 / B)

    return gaps, sks, log_Wks


采用核函数
基于欧式距离的 K-means 假设了了各个数据簇的数据具有一样的的先验概率并呈现球形分布,但这种分布在实际生活中并不常见。面对非凸的数据分布形状时我们可以引入核函数来优化,这时算法又称为核 K-means 算法,是核聚类方法的一种。核聚类方法的主要思想是通过一个非线性映射,将输入空间中的数据点映射到高位的特征空间中,并在新的特征空间中进行聚类。非线性映射增加了数据点线性可分的概率,从而在经典的聚类算法失效的情况下,通过引入核函数可以达到更为准确的聚类结果。

 K-means++算法 :改进的算法

原始K-means算法最开始随机选取数据集中K个点作为聚类中心,而K-means++按照如下的思想选取K个聚类中心:假设已经选取了n个初始聚类中心(0<n<K),则在选取第n+1个聚类中心时:距离当前n个聚类中心越远的点会有更高的概率被选为第n+1个聚类中心。在选取第一个聚类中心(n=1)时同样通过随机的方法。可以说这也符合我们的直觉:聚类中心当然是互相离得越远越好。这个改进虽然直观简单,但是却非常得有效。

  1. 随机选取一个中心点 \(a_{1}\) ;
  2. 计算数据到之前 \(\mathrm{n}\) 个聚类中心最远的距离 \(D(x)\) ,并以一定概率 \(\frac{D(x)^{2}}{\sum D(x)^{2}}选择新中心点 a_{i} ;\)
  3. 重复第二步。

但是这个算法的缺点在于,难以并行化。所以 k-means II 改变取样策略,并非按照 k-means++ 那样每次遍历只取样一个样本,而是每次遍历取样 k 个,重复该取样过程 log(n)次,则得到 klog(n)个样本点组成的集合,然后从这些点中选取 k 个。当然一般也不需要 log(n)次取样,5 次即可。

K-means与ISODATA:ISODATA的全称是迭代自组织数据分析法。在K-means中,K的值需要预先人为地确定,并且在整个算法过程中无法更改。而当遇到高维度、海量的数据集时,人们往往很难准确地估计出K的大小。ISODATA就是针对这个问题进行了改进,它的思想也很直观:当属于某个类别的样本数过少时把这个类别去除,当属于某个类别的样本数过多、分散程度较大时把这个类别分为两个子类别。

K-means与Kernel K-means:传统K-means采用欧式距离进行样本间的相似度度量,显然并不是所有的数据集都适用于这种度量方式。参照支持向量机中核函数的思想,将所有样本映射到另外一个特征空间中再进行聚类,就有可能改善聚类效果。

wordpress插入数学公式

这段时间写了几篇machine learning的文章,需要用到很多数学公式,这些文章里所有的数学公式基本都是贴图的,其实这样并不方便,而且很不美观。主要是自己懒,觉得能截图就截图了,感觉在wordpress里编辑数学公式肯定很麻烦,但实际上在wordpress里显示数学公式是一件非常简单的事,出乎意料的简单,连插件都不需要装。我采用的方案是一个基于LaTeX显示数学公式的JavaScript引擎-MathJax-https://www.mathjax.org/,这个JS引擎的优点是全浏览器支持,不需要额外插件设置,非常方便。具体使用步骤如下:在header.php文件里添加JS引用,具体步骤:登陆wordpress,进入外观->编辑->主题页眉 (header.php)。在head标签里添加一行代码引入MathJax:

1<script src=’https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-MML-AM_CHTML’></script>

值得注意的是,这行代码必须要放到 <?php wp_head(); ?>之前,否则不生效)这里的JS地址用的是MathJax的官方CDN,其实也完全可以把这个MathJax.js文件下载放到本地,然后配置成本地地址。但是呢,这个CDN地址在墙内的访问速度还可以,所以还是建议大家直接使用官方的CDN吧,省事、方便。



完成上面的步骤就完成了MathJax的配置,怎么样,很简单吧。那配置好了之后该如何显示数学公式呢?我们知道MathJax是基于LaTeX的,所以首先要用LaTeX语法把数学公式写出来。这里推荐一个非常好用的在线的LaTeX编辑数学公式的网站:Oneline LaTex Equation Editor
这个网站提供图形化编辑工具,能实时显示数学公式。LaTeX编辑数学公式的语法学习成本基本为零,很简单,写得多了都不需要图形化工具了。用LaTex写好了数学公式之后在博客里把它加进去,这里要使用某些特定的分隔符以方便被MathJax识别。具体地,有两种显示方式:

  • 换行显示(displayed mathematics),它的分隔符是 $ $…$ $和 \[…\ ,比如我们有一个数学公式: \sqrt{a^2+b^2} ,那么它的换行显示的格式就是: $$\sqrt{a^2+b^2}$$ 或者 \[\sqrt{a^2+b^2}\] ,显示效果就是这样的:

行内显示(in-line mathematics),它的分割符号是 \(...\) ,行内显示的格式: \(\sqrt{a^2+b^2}\)

python 多个解释器如何切换和安装包

今天遇到一个问题,在linux里有好几个python解释器

/bin/python3

/usr/bin/python

/bin/python

查看python解释器路径:

通过脚本查看

import sys
import os 
print('当前 Python 解释器路径:')
print(sys.executable)

如果需要修改默认Python解释器:
默认的python解释器是python3.6或者python2.7,反正不是我们刚下载下来的python3.7。为把默认的python解释器改为python3.7,我们进行如下操作:

cd /usr/bin
sudo rm /usr/bin/python #删除原有的python连接文件
sudo ln -s /usr/bin/python3.7 python #建立指向python3.7的连接

将包安装到指定python解释器:

用pip命令把python包安装到指定目录:

sudo pip install numpy –target=/usr/local/lib/python2.7/site-packages

支持向量机 –SVM

参考文章:支持向量机通俗导论

https://blog.csdn.net/v_july_v/article/details/7624837

冬至:白天最是时光短,凛冽寒冬早归家

 线性分类
在训练数据中,每个数据都有n个的属性和一个二类类别标志,我们可以认为这些数据在一个n维空间里。我们的目标是找到一个n-1维的超平面(hyperplane),这个超平面可以将数据分成两部分,每部分数据都属于同一个类别。其实这样的超平面有很多,我们要找到一个最佳的。因此,增加一个约束条件:这个超平面到每边最近数据点的距离是最大的。也成为最大间隔超平面(maximum-margin hyperplane)。这个分类器也成为最大间隔分类器(maximum-margin classifier)。支持向量机是一个二类分类器。

非线性分类
SVM的一个优势是支持非线性分类。它结合使用拉格朗日乘子法和KKT条件,以及核函数可以产生非线性分类器。

以下摘自 支持向量机通俗导论 https://blog.csdn.net/v_july_v/article/details/7624837

支持向量机,因其英文名为 Support Vector Machine,故一般简称 SVM,通俗
来讲,它是一种二类分类模型,其基本模型定义为特征空间上的间隔最大的线
性分类器,其学习策略便是间隔最大化,最终可转化为一个凸二次规划问题的求解。

凸二次优化https://zhuanlan.zhihu.com/p/100041443

凸函数:直观来讲就是形状看上去“凹”下去的函数,注意可不是看上去“凸”的函数

凸优化的形式化定义:

其中, x 为决策变量, f和g 均为凸函数, h为仿射(线性)函数。 

拉格朗日对偶(Lagrange duality)

https://www.cnblogs.com/90zeng/p/Lagrange_duality.html

  KKT条件

同时包含等式约束和不等式约束:

定义Lagrange函数:

此时 

[公式]

 满足的必要条件为:

大名鼎鼎的SVM算法。机器学习发展历史上一颗璀璨的明珠。相信很多接触机器学习的同学都是从SVM开始的。

SVM是为小样本学习设计的,而工业界(尤其是互联网领域)不缺少数据,同时SVM训练效率较低且不容易调试,同时不如LR模型可解释行强。所以SVM常见于实验室而在互联网领域鲜有应用。不过SVM将问题建模为“有约束凸二次优化问题”,其求解过程非常具有代表性。

上述过程将原有问题通过拉格朗日乘子法转换为对偶问题。

上述过程通过各种消元trick将问题转换成只有一类对偶变量 [公式] 的形式,减小了求解难度。在实际SVM工具包中通常采用SMO算法。

如果所有变量的解都满足此最优化问题的KKT条件(Karush-Kuhn-Tucker conditions),那么这个最优化问题的解就得到了。因为KKT条件是该最优化问题的充分必要条件。否则,选择两个变量,固定其他变量,针对这两个变量构建一个二次规划问题。这个二次规划问题关于这两个变量的解应该更接近原始二次规划问题的解,因为这会使得原始二次规划问题的目标函数值变得更小。重要的是,这时子问题可以通过解析方法求解,这样就可以大大提高整个算法的计算速度。子问题有两个变量,一个是违反KKT条件最严重的那一个,另一个由约束条件自动确定。如此,SMO算法将原问题不断分解为子问题并对子问题求解,进而达到求解原问题的目的。

Linux文件分区和挂载相关知识

之前只是在实验室的服务器上跑代码,但因为将anaconda文件夹放置在了系统盘110G,所以系统爆满,想要解决,却无从下手,因此将linux中涉及文件挂载分区的知识进行整理总结,以备不时之需。

磁盘分区:

Linux 物理磁盘管理常用三个命令为 dfdu 和 fdisk

  • df(英文全称:disk full):列出文件系统的整体磁盘使用量
  • du(英文全称:disk used):检查磁盘空间使用量
  • fdisk:用于磁盘分区

每次安装系统的时候我们都会进行分区,Linux下磁盘分区和目录的关系如下:

  • 任何一个分区都必须挂载到某个目录上。
  • 目录是逻辑上的区分。分区是物理上的区分(比如有个一1T的硬盘,我们先通过fdisk进行磁盘分区,并使用字母和数字的组合来指代磁盘分区,比如/dev/xxyN)。
  • 磁盘Linux分区都必须挂载到目录树中的某个具体的目录上才能进行读写操作。
  • 根目录是所有Linux的文件和目录所在的地方,需要挂载上一个磁盘分区

mount挂载


挂载的概念
当要使用某个设备时,例如要读取硬盘中的一个格式化好的分区、光盘或软件等设备时,必须先把这些设备对应到某个目录(这个目录可以不为空,但挂载后这个目录下以前的内容将不可用),而这个目录就称为“挂载点(mount point)”,这样才可以读取这些设备,而这些对应的动作就是“挂载”。
需要理解的是:
linux操作系统将所有的设备都看作文件,
它将整个计算机的资源都整合成一个大的文件目录。
我们要访问存储设备中的文件,必须将文件所在的分区挂载到一个已存在的目录上,
然后通过访问这个目录来访问存储设备

挂载分区:挂载的目录必须为空

mkdir /d1
mkdir /d2
mount /dev/sdc1 /d1
mount /dev/sdc2 /d2

查看文件(夹)所在分区(挂载点):

1、最简单的,直接 df  <文件(夹)路径>

2、用df 或 fdisk -l查看分区挂载情况,直接输入mount或者也可以用cat /etc/mtab,然后pwd找最接近的挂载点信息

Linux修改磁盘挂载目录

  比如想把已经挂载在home目录上的硬盘挂载到data目录上, 如下操作

  #df -h(查看分区情况及数据盘名称)

  # mkdir /data(如果没有data目录就创建,否则此步跳过)

  # umount /home(卸载硬盘已挂载的home目录)

  # mount /dev/sdb3 /data (挂载到data目录)

  # vi /etc/fstab (编辑fstab文件修改或添加,使重启后可以自动挂载)

  /dev/sdb3 /data ext3 auto 0 0

  数据盘 新挂载目录

  编辑/etc/fstab里面的/home为/data, 或创建让系统启动的时候自动挂载到/data


ln链接 命令—-linux常用命令

ln -s

你可以将链接简单地理解为 Windows 中常见的快捷方式(或是 OS X 中的替身),Linux 中常用它来解决一些库版本的问题,通常也会将一些目录层次较深的文件链接到一个更易访问的目录中。在这些用途上,我们通常会使用到软链接(也称符号链接)。

我为什么要用:有时候服务器的home目录下的磁盘容量不足,而用户的文件都在 /home/用户名下面,这样的话无法满足用户的使用,就需要ln软链接了

建立软连接

ln [参数][源文件或目录][目标文件或目录]
常用:软链接命令
ln -s  源文件或目录  目标文件或目录 (将源文件或目录 连接到 目标文件或目录)
其中:
源文件或目录:文件原始地址
目标文件或目录:文件想要建立链接的位置

Linux ln(英文全拼:link files)命令是一个非常重要命令,它的功能是为某一个文件在另外一个位置建立一个同步的链接。

当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在某个固定的目录,放上该文件,然后在 其它的目录下用ln命令链接(link)它就可以,不必重复的占用磁盘空间。

命令功能 :
Linux文件系统中,有所谓的链接(link),我们可以将其视为档案的别名,而链接又可分为两种 : 硬链接(hard link)与软链接(symbolic link),硬链接的意思是一个档案可以有多个名称,而软链接的方式则是产生一个特殊的档案,该档案的内容是指向另一个档案的位置。硬链接是存在同一个文件系统中,而软链接却可以跨越不同的文件系统。

不论是硬链接或软链接都不会将原本的档案复制一份,只会占用非常少量的磁碟空间。

软链接

  • 1.软链接,以路径的形式存在。类似于Windows操作系统中的快捷方式
  • 2.软链接可以 跨文件系统 ,硬链接不可以
  • 3.软链接可以对一个不存在的文件名进行链接
  • 4.软链接可以对目录进行链接

硬链接

  • 1.硬链接,以文件副本的形式存在。但不占用实际空间。
  • 2.不允许给目录创建硬链接
  • 3.硬链接只有在同一个文件系统中才能创建

必要参数

  • -b 删除,覆盖以前建立的链接
  • -d 允许超级用户制作目录的硬链接
  • -f 强制执行
  • -i 交互模式,文件存在则提示用户是否覆盖
  • -n 把符号链接视为一般目录
  • -s 软链接(符号链接)
  • -v 显示详细的处理过程

这里有两点要注意:

第一,ln命令会保持每一处链接文件的同步性,也就是说,不论你改动了哪一处,其它的文件都会发生相同的变化;

第二,ln的链接又分软链接和硬链接两种,软链接就是ln –s 源文件 目标文件,它只会在你选定的位置上生成一个文件的镜像,不会占用磁盘空间,硬链接 ln 源文件 目标文件,没有参数-s, 它会在你选定的位置上生成一个和源文件大小相同的文件,无论是软链接还是硬链接,文件都保持同步变化。

ln指令用在链接文件或目录,如同时指定两个以上的文件或目录,且最后的目的地是一个已经存在的目录,则会把前面指定的所有文件或目录复制到该目录中。若同时指定多个文件或目录,且最后的目的地并非是一个已存在的目录,则会出现错误信息。

删除软连接的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 以下这样的删除都没问题
~ unlink link
~ rm link
~ rm -r link # 这里的参数 r 其实是没有意义的,因为link是一个软连接 不是目录
~ rm -rf link # 这里的 rf 同样没有意义,只是rm 命令忽略了这里的参数
~
~
# 这样删除就会造成灾难
~ rm -rf link/
# 这个时候你发现软连接并没有删除,但是 origin 目录下的文件是全部没删除了 ==!
# 这些罪魁祸首是 参数 f,如果你没有使用f参数 这一切还可以挽回
~ rm link/
rm: cannot remove `link/’: Is a directory
# 这里 rm 通过你的参数 link/ 发现是要删除一个目录,这时候需要你添加参数 r
~ rm -r link/
rm: cannot remove `link’: Not a directory
# 这里你添加了 r 参数,但是并不能找到目录 link/ 因为link并不是一个目录,他是一个软连接,只不过有些shell在补全的时候会将 `/` 补全上去

collections模块

https://pixabay.com/

collections模块实现一些特定的数据类型,可以替代Python中常用的内置数据类型如dict, list, set, tuple,简单说就是对基本数据类型做了更上一层的处理。

https://github.com/python/cpython/blob/3.10/Lib/collections/init.py

https://docs.python.org/zh-cn/3/library/collections.html?highlight=deque#collections.namedtuple

init.py:

'''This module implements specialized container datatypes providing
alternatives to Python's general purpose built-in containers, dict,
list, set, and tuple.

* namedtuple   factory function for creating tuple subclasses with named fields
* deque        list-like container with fast appends and pops on either end
* ChainMap     dict-like class for creating a single view of multiple mappings
* Counter      dict subclass for counting hashable objects
* OrderedDict  dict subclass that remembers the order entries were added
* defaultdict  dict subclass that calls a factory function to supply missing values
* UserDict     wrapper around dictionary objects for easier dict subclassing
* UserList     wrapper around list objects for easier list subclassing
* UserString   wrapper around string objects for easier string subclassing

'''

__all__ = ['deque', 'defaultdict', 'namedtuple', 'UserDict', 'UserList',
            'UserString', 'Counter', 'OrderedDict', 'ChainMap']

一、deque

用途:双端队列,头部和尾部都能以O(1)时间复杂度插入和删除元素。类似于列表的容器

所谓双端队列,就是两端都能操作,与Python内置的list区别在于:头部插入与删除的时间复杂度为O(1),来个栗子感受一下:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'

"""
保留最后n个元素
"""
from collections import deque


def search(file, pattern, history=5):
    previous_lines = deque(maxlen=history)
    for l in file:
        if pattern in l:
            yield l, previous_lines  # 使用yield表达式的生成器函数,将搜索过程的代码和搜索结果的代码解耦
        previous_lines.append(l)


with open(b'file.txt', mode='r', encoding='utf-8') as f:
    for line, prevlines in search(f, 'Python', 5):
        for pline in prevlines:
            print(pline, end='')
        print(line, end='')

d = deque()
d.append(1)
d.append("2")
print(len(d))
print(d[0], d[1])
d.extendleft([0])
print(d)
d.extend([6, 7, 8])
print(d)

d2 = deque('12345')
print(len(d2))
d2.popleft()
print(d2)
d2.pop()
print(d2)

# 在队列两端插入或删除元素时间复杂度都是 O(1) ,区别于列表,在列表的开头插入或删除元素的时间复杂度为 O(N)
d3 = deque(maxlen=2)
d3.append(1)
d3.append(2)
print(d3)
d3.append(3)
print(d3)

输出结果如下

人生苦短
我用Python
2
1 2
deque([0, 1, '2'])
deque([0, 1, '2', 6, 7, 8])
5
deque(['2', '3', '4', '5'])
deque(['2', '3', '4'])
deque([1, 2], maxlen=2)
deque([2, 3], maxlen=2)

因此,如果你遇到经常操作列表头的场景,使用deque最好。deque类的所有方法,自行操作一遍就知道了。

class deque(object):
    """
    deque([iterable[, maxlen]]) --> deque object

    A list-like sequence optimized for data accesses near its endpoints.
    """
    def append(self, *args, **kwargs): # real signature unknown
        """ Add an element to the right side of the deque. """
        pass

    def appendleft(self, *args, **kwargs): # real signature unknown
        """ Add an element to the left side of the deque. """
        pass

    def clear(self, *args, **kwargs): # real signature unknown
        """ Remove all elements from the deque. """
        pass

    def copy(self, *args, **kwargs): # real signature unknown
        """ Return a shallow copy of a deque. """
        pass

    def count(self, value): # real signature unknown; restored from __doc__
        """ D.count(value) -> integer -- return number of occurrences of value """
        return 0

    def extend(self, *args, **kwargs): # real signature unknown
        """ Extend the right side of the deque with elements from the iterable """
        pass

    def extendleft(self, *args, **kwargs): # real signature unknown
        """ Extend the left side of the deque with elements from the iterable """
        pass

    def index(self, value, start=None, stop=None): # real signature unknown; restored from __doc__
        """
        D.index(value, [start, [stop]]) -> integer -- return first index of value.
        Raises ValueError if the value is not present.
        """
        return 0

    def insert(self, index, p_object): # real signature unknown; restored from __doc__
        """ D.insert(index, object) -- insert object before index """
        pass

    def pop(self, *args, **kwargs): # real signature unknown
        """ Remove and return the rightmost element. """
        pass

    def popleft(self, *args, **kwargs): # real signature unknown
        """ Remove and return the leftmost element. """
        pass

    def remove(self, value): # real signature unknown; restored from __doc__
        """ D.remove(value) -- remove first occurrence of value. """
        pass

    def reverse(self): # real signature unknown; restored from __doc__
        """ D.reverse() -- reverse *IN PLACE* """
        pass

    def rotate(self, *args, **kwargs): # real signature unknown
        """ Rotate the deque n steps to the right (default n=1).  If n is negative, rotates left. """
        pass

这里提示一下,有些函数对队列进行操作,但返回值是None,比如reverse()反转队列,rotate(1)将队列中元素向右移1位,尾部的元素移到头部。

二、defaultdict

用途:带有默认值的字典。父类为Python内置的dict

字典带默认值有啥好处?举个栗子,一般来讲,创建一个多值映射字典是很简单的。但是,如果你选择自己实现的话, 那么对于值的初始化可能会有点麻烦,你可能会像下面这样来实现:

d = {}
for key, value in pairs:
    if key not in d:
        d[key] = []
    d[key].append(value)

如果使用 defaultdict 的话代码就更加简洁了:

d = defaultdict(list)
for key, value in pairs:
    d[key].append(value)

defaultdict 的一个特征是它会自动初始化每个 key 刚开始对应的值,所以你只需要 关注添加元素操作了。比如:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'

# 字典中的键映射多个值
from collections import defaultdict

d = defaultdict(list)
print(d)
d['a'].append([1, 2, 3])
d['b'].append(2)
d['c'].append(3)

print(d)

d = defaultdict(set)
print(d)
d['a'].add(1)
d['a'].add(2)
d['b'].add(4)

print(d)

输出结果如下:

defaultdict(<class 'list'>, {})
defaultdict(<class 'list'>, {'a': [[1, 2, 3]], 'b': [2], 'c': [3]})
defaultdict(<class 'set'>, {})
defaultdict(<class 'set'>, {'a': {1, 2}, 'b': {4}})

三、namedtuple()

用途:创建命名字段的元组。工厂函数

namedtuple主要用来产生可以使用名称来访问元素的数据对象,通常用来增强代码的可读性, 在访问一些tuple类型的数据时尤其好用。

比如我们用户拥有一个这样的数据结构,每一个对象是拥有三个元素的tuple。使用namedtuple方法就可以方便的通过tuple来生成可读性更高也更好用的数据结构。

from collections import namedtuple

websites = [
    ('Sohu', 'http://www.sohu.com/', u'张朝阳'),
    ('Sina', 'http://www.sina.com.cn/', u'王志东'),
    ('163', 'http://www.163.com/', u'丁磊')
]

Website = namedtuple('Website', ['name', 'url', 'founder'])

for website in websites:
    website = Website._make(website)
    print website


# 输出结果:
Website(name='Sohu', url='http://www.sohu.com/', founder=u'\u5f20\u671d\u9633')
Website(name='Sina', url='http://www.sina.com.cn/', founder=u'\u738b\u5fd7\u4e1c')
Website(name='163', url='http://www.163.com/', founder=u'\u4e01\u78ca')

注意,namedtuple是函数,不是类。

四、Counter

用途:统计可哈希的对象。父类为Python内置的dict

寻找序列中出现次数最多的元素。假设你有一个单词列表并且想找出哪个单词出现频率最高:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'

from collections import Counter

words = [
    'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
    'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the',
    'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into',
    'my', 'eyes', "you're", 'under'
]

word_counts = Counter(words)

# 出现频率最高的三个单词
top_three = word_counts.most_common(3)
print(top_three)
# Outputs [('eyes', 8), ('the', 5), ('look', 4)]
print(word_counts['eyes'])

morewords = ['why', 'are', 'you', 'not', 'looking', 'in', 'my', 'eyes']

# 如果你想手动增加计数,可以简单的用加法:
for word in morewords:
    print(word)
    word_counts[word] += 1
print(word_counts['eyes'])

结果如下:

[('eyes', 8), ('the', 5), ('look', 4)]
8
why
are
you
not
looking
in
my
eyes
9

因为Counter继承自dict,所有dict有的方法它都有(defaultdict和OrderedDict也是的),Counter自己实现或重写了6个方法:

  • most_common(self, n=None),
  • elements(self)
  • fromkeys(cls, iterable, v=None)
  • update(*args, **kwds)
  • subtract(*args, **kwds)
  • copy(self)

五、OrderedDict

用途:排序的字段。父类为Python内置的dict

OrderedDict在迭代操作的时候会保持元素被插入时的顺序,OrderedDict内部维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来的时候,它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。

需要注意的是,一个OrderedDict的大小是一个普通字典的两倍,因为它内部维护着另外一个链表。 所以如果你要构建一个需要大量OrderedDict 实例的数据结构的时候(比如读取100,000行CSV数据到一个 OrderedDict 列表中去),那么你就得仔细权衡一下是否使用 OrderedDict带来的好处要大过额外内存消耗的影响。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'

from collections import OrderedDict

d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4
# d['bar'] = 22 #对于一个已经存在的键,重复赋值不会改变键的顺序
for key in d:
    print(key, d[key])

print(d)

import json

print(json.dumps(d))

结果如下:

foo 1
bar 2
spam 3
grok 4
OrderedDict([('foo', 1), ('bar', 2), ('spam', 3), ('grok', 4)])
{"foo": 1, "bar": 2, "spam": 3, "grok": 4}

OrderDict实现或重写了如下方法。都是干嘛的?这个留给大家当课后作业了^_^

  • clear(self)
  • popitem(self, last=True)
  • move_to_end(self, key, last=True)
  • keys(self)
  • items(self)
  • values(self)
  • pop(self, key, default=__marker)
  • setdefault(self, key, default=None)
  • copy(self)
  • fromkeys(cls, iterable, value=None)

六、ChainMap

用途:创建多个可迭代对象的集合。类字典类型

很简单,如下:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'

from collections import ChainMap
from itertools import chain

# 不同集合上元素的迭代
a = [1, 2, 3, 4]
b = ('x', 'y', 'z')
c = {1, 'a'}

# 方法一,使用chain
for i in chain(a, b, c):
    print(i)
print('--------------')
# 方法二,使用chainmap
for j in ChainMap(a, b, c):
    print(j)

# 这两种均为节省内存,效率更高的迭代方式

一个 ChainMap 接受多个字典并将它们在逻辑上变为一个字典。然后,这些字典并不是真的合并在一起了,ChainMap 类只是在内部创建了一个容纳这些字典的列表并重新定义了一些常见的字典操作来遍历这个列表。大部分字典操作都是可以正常使用的,比如:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = 'liao gao xiang'

# 合并多个字典和映射
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
# 现在假设你必须在两个字典中执行查找操作
# (比如先从 a 中找,如果找不到再在 b 中找)。
# 一个非常简单的解决方案就是使用collections模块中的ChainMap类
from collections import ChainMap

c = ChainMap(a, b)

print(c)
a['x'] = 11  # 使用ChainMap时,原字典做了更新,这种更新会合并到新的字典中去

print(c)  # 按顺序合并两个字典
print(c['x'])
print(c['y'])
print(c['z'])

# 对于字典的更新或删除操作影响的总是列中的第一个字典。
c['z'] = 10
c['w'] = 40
del c['x']
print(a)
# del c['y']将出现报错

# ChainMap对于编程语言中的作用范围变量(比如globals,locals等)
# 是非常有用的。事实上,有一些方法可以使它变得简单:
values = ChainMap()  # 默认会创建一个空字典
print('\t', values)
values['x'] = 1
values = values.new_child()  # 添加一个空字典
values['x'] = 2
values = values.new_child()
values['x'] = 30
# values = values.new_child()
print(values, values['x'])  # values['x']输出最后一次添加的值
values = values.parents  # 删除上一次添加的字典
print(values['x'])
values = values.parents
print(values)

a = {'x': 1, 'y': 2}
b = {'y': 2, 'z': 3}
merge = dict(b)
merge.update(a)
print(merge['x'], merge['y'], merge['z'])
a['x'] = 11
print(merge['x'])

输出结果如下:

ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4})
ChainMap({'x': 11, 'z': 3}, {'y': 2, 'z': 4})
11
2
3
{'z': 10, 'w': 40}
     ChainMap({})
ChainMap({'x': 30}, {'x': 2}, {'x': 1}) 30
2
ChainMap({'x': 1})
1 2 3
1

作为ChainMap的替代,你可能会考虑使用 update() 方法将两个字典合并。这样也能行得通,但是它需要你创建一个完全不同的字典对象(或者是破坏现有字典结构)。同时,如果原字典做了更新,这种改变不会反应到新的合并字典中去。

ChainMap实现或重写了如下方法:

  • get(self, key, default=None)
  • fromkeys(cls, iterable, *args)
  • copy(self)
  • new_child(self, m=None)
  • parents(self)
  • popitem(self)
  • pop(self, key, *args)
  • clear(self)

七、UserDict、UserList、UserString

这三个类是分别对 dict、list、str 三种数据类型的包装,其主要是为方便用户实现自己的数据类型。在 Python2 之前,这三个类分别位于 UserDict、UserList、UserString 三个模块中,需要用类似于 from UserDict import UserDict 的方式导入。在 Python3 之后则被挪到了 collections 模块中。这三个类都是基类,如果用户要扩展这三种类型,只需继承这三个类即可。