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

支持向量机 –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算法将原问题不断分解为子问题并对子问题求解,进而达到求解原问题的目的。

编码器—解码器(seq2seq)

当输入和输出都是不定长序列时(比如机器翻译),我们可以使用编码器—解码器(encoder-decoder) 或者seq2seq模型。这两个模型本质上都用到了两个循环神经网络,分别叫做编码器和解码器。编码器用来分析输入序列,解码器用来生成输出序列。

以机器翻译为例,输入可以是一段不定长的英语文本序列,输出可以是一段不定长的法语文本序列,例如

英语输入:“They”、“are”、“watching”、“.”

法语输出:“Ils”、“regardent”、“.”

下图描述了使用编码器—解码器将上述英语句子翻译成法语句子的一种方法。在训练数据集中,我们可以在每个句子后附上特殊符号“<eos>”(end of sequence)以表示序列的终止。编码器每个时间步的输入依次为英语句子中的单词、标点和特殊符号“<eos>”。下图中使用了编码器在最终时间步的隐藏状态作为输入句子的表征或编码信息。解码器在各个时间步中使用输入句子的编码信息和上个时间步的输出以及隐藏状态作为输入。我们希望解码器在各个时间步能正确依次输出翻译后的法语单词、标点和特殊符号”<eos>”。需要注意的是,解码器在最初时间步的输入用到了一个表示序列开始的特殊符号”<bos>”(beginning of sequence)。

使用编码器—解码器将句子由英语翻译成法语。编码器和解码器分别为循环神经网络

编码器的作用是把一个不定长的输入序列变换成一个定长的背景变量c,并在该背景变量中编码输入序列信息。常用的编码器是循环神经网络。

根据最大似然估计,我们可以最大化输出序列基于输入序列的条件概率,并得到该输出序列的损失,在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。

个人理解:

最后decoder每一步输出的是一个字典大小的概率值向量,分别表示这一步输出所有值的概率,一般取最大的值作为输出。

字典大小为4

因此那么在模型预测的时候就需要进行搜索,选择不同的搜索方式决定每一时间步的输出是字典里的那个值,选择不同的值会影响下一时间步的输出概率。

模型预测 Model Prediction

为了搜索该条件概率最大的输出序列,一种方法是穷举所有可能输出序列的条件概率,并输出条件概率最大的序列。我们将该序列称为最优序列,并将这种搜索方法称为穷举搜索 (exhaustive search)。

贪婪搜索 Greedy Search

我们还可以使用贪婪搜索 (greedy search) 。也就是说,对于输出序列任一时间步 [公式] ,从 [公式] 个词中搜索出输出词

[公式]

且一旦搜索出 “<eos>” 符号即完成输出序列。贪婪搜索的计算开销是 [公式] 。它比起穷举搜索的计算开销显著下降。例如,当 [公式] 且 [公式] 时,我们只需评估 [公式] 个序列。

下面我们来看一个例子。假设输出词典里面有 “A”、“B”、“C”和 “<eos>” 这四个词。下图中每个时间步下的四个数字分别代表了该时间步生成 “A”、“B”、“C”和 “<eos>” 这四个词的条件概率。在每个时间步,贪婪搜索选取生成条件概率最大的词。因此,将生成序列 “ABC<eos>” 。该输出序列的条件概率是 [公式] 。

束搜索 Beam Search

束搜索 (beam search) 是比贪婪搜索更加广义的搜索算法。它有一个束宽 (beam size) 超参数。我们将它设为 [公式] 。在时间步1时,选取当前时间步生成条件概率最大的 [公式] 个词,分别组成 [公式] 个候选输出序列的首词。在之后的每个时间步,基于上个时间步的 [公式] 个候选输出序列,从 [公式] 个可能的输出序列中选取生成条件概率最大的 [公式] 个,作为该时间步的候选输出序列。

最终,我们在各个时间步的候选输出序列中筛选出包含特殊符号 “<eos>” 的序列,并将它们中所有特殊符号 “<eos>” 后面的子序列舍弃,得到最终候选输出序列。在这些最终候选输出序列中,取以下分数最高的序列作为输出序列:

[公式]
[公式]

其中 [公式] 为最终候选序列长度, [公式] 一般可选为0.75。分母上的 是为了惩罚较长序列在以上分数中较多的对数相加项。分析可得,束搜索的计算开销为 [公式] 。这介于穷举搜索和贪婪搜索的计算开销之间。

  • 预测不定长序列的方法包括贪婪搜索、穷举搜索和束搜索。
  • 束搜索通过灵活的束宽来权衡计算开销和搜索质量。

注意力机制

在普通的编码器-解码器模型中,有一个很大的局限性。那就是上下文变量对于 Decoding 阶段每个时间步都是一样的,这可能是模型性能的一个瓶颈。我们希望不同时间步的解码能够依赖于与之更相关的上下文信息,换句话说,Decoding 往往并不需要整个输入序列的信息,而是要有所侧重。于是,Bengio 团队的 Bahdanau 在 2014年首次在编码器-解码器模型中引入了注意力机制 (Attention Mechanism):

注意力机制通过注意力汇聚将查询(自主性提示)和键(非自主性提示)结合在一起,实现对值(感官输入)的选择倾向

动机 Motivation

以英语-法语翻译为例,给定一对英语输入序列 “They”、“are”、“watching”、“.” 和法语输出序列 “Ils”、“regardent”、“.”。解码器可以在输出序列的时间步1使用更集中编码了 “They”、“are” 信息的上下文变量来生成 “Ils”,在时间步2使用更集中编码了 “watching” 信息的上下文变量来生成“regardent”,在时间步3使用更集中编码了 “.” 信息的上下文变量来生成 “.”。这看上去就像是在解码器的每一时间步对输入序列中不同时间步编码的信息分配不同的注意力。这也是注意力机制的由来。它最早由 Bahanau 等人提出。

仍然以循环神经网络为例,注意力机制通过对编码器所有时间步的隐藏状态做加权平均来得到背景变量。解码器在每一时间步调整这些权重,即注意力权重,从而能够在不同时间步分别关注输入序列中的不同部分并编码进相应时间步的背景变量。

我们先描述第一个关键点,即计算背景变量。下图描绘了注意力机制如何为解码器在时间步2计算背景变量。首先,函数a根据解码器在时间步1的隐藏状态和编码器在各个时间步的隐藏状态计算softmax运算的输入。softmax运算输出概率分布并对编码器各个时间步的隐藏状态做加权平均,从而得到背景变量。

本质上,注意力机制能够为表征中较有价值的部分分配较多的计算资源。这个有趣的想法自提出后得到了快速发展,特别是启发了依靠注意力机制来编码输入序列并解码出输出序列的变换器(Transformer)模型的设计 [2]。变换器抛弃了卷积神经网络和循环神经网络的架构。它在计算效率上比基于循环神经网络的编码器—解码器模型通常更具明显优势。含注意力机制的变换器的编码结构在后来的BERT预训练模型中得以应用并令后者大放异彩:微调后的模型在多达11项自然语言处理任务中取得了当时最先进的结果 [3]。不久后,同样是基于变换器设计的GPT-2模型于新收集的语料数据集预训练后,在7个未参与训练的语言模型数据集上均取得了当时最先进的结果 [4]。除了自然语言处理领域,注意力机制还被广泛用于图像分类、自动图像描述、唇语解读以及语音识别

评价机器翻译结果

评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)[1]。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。

总结 Conclusions

让我们回顾一下带注意力机制的编码器-解码器的整个设计:

  1. Encoder 总结输入序列的信息,得到上下文变量 [公式]
  2. Decoder 将上下文变量 [公式] 中的信息解码生成输出序列
  3. 设计 [公式] 函数
  4. 计算当前时间步的隐藏状态 [公式]
  5. 计算当前时间步的解码器输出概率 [公式]
  6. 得到输出序列的联合概率 [公式] 并最大化
  7. 根据 MLE,就是最小化联合概率的负对数
  8. 得到 loss function
  9. 用优化方法降低 loss,学习模型参数
  10. 为了避免相同的上下文变量对模型性能的限制,给编码器-解码器模型加入了注意力机制。

Convolutional Neural Networks for Sentence Classification

https://arxiv.org/abs/1408.5882

github实现

https://github.com/yoonkim/CNN_sentence

https://github.com/Cheneng/TextCNN

对于文本分类,我们能不能用CNN来做,用某种模型初始化,进而做fine-tune呢?答案是肯定的,用于文本分析的CNN—TextCNN。

text-cnn用于情感分类:

与二维卷积层一样,一维卷积层使用一维的互相关运算。在一维互相关运算中,卷积窗口从输入数组的最左方开始,按从左往右的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。

多输入通道的一维互相关运算也与多输入通道的二维互相关运算类似:在每个通道上,将核与相应的输入做一维互相关运算,并将通道之间的结果相加得到输出结果。

由二维互相关运算的定义可知,多输入通道的一维互相关运算可以看作单输入通道的二维互相关运算。

类似地,我们有一维池化层。textCNN中使用的时序最大池化(max-over-time pooling)层实际上对应一维全局最大池化层:假设输入包含多个通道,各通道由不同时间步上的数值组成,各通道的输出即该通道所有时间步中最大的数值。因此,时序最大池化层的输入在各个通道上的时间步数可以不同。

简单来说,时序最大池化层就是沿着时序方向进行最大池化。

textCNN模型主要使用了一维卷积层和时序最大池化层。假设输入的文本序列由n个词组成,每个词用d维的词向量表示。那么输入样本的宽为n,高为1,输入通道数为d。textCNN的计算主要分为以下几步。(输入通道就是每个词的d为维度表示,宽就是时序长度)

词用d维的词向量表示 :一般使用词嵌入模型word2vec.

  1. 定义多个一维卷积核,并使用这些卷积核对输入分别做卷积计算。宽度不同的卷积核可能会捕捉到不同个数的相邻词的相关性。
  2. 对输出的所有通道分别做时序最大池化,再将这些通道的池化输出值连结为向量。
  3. 通过全连接层将连结后的向量变换为有关各类别的输出。这一步可以使用丢弃层应对过拟合。

下图用一个例子解释了textCNN的设计。这里的输入是一个有11个词的句子,每个词用6维词向量表示。因此输入序列的宽为11,输入通道数为6。给定2个一维卷积核,核宽分别为2和4,输出通道数分别设为4和5。因此,一维卷积计算后,4个输出通道的宽为11−2+1=10,而其他5个通道的宽为11−4+1=8。尽管每个通道的宽不同,我们依然可以对各个通道做时序最大池化,并将9个通道的池化输出连结成一个9维向量。最终,使用全连接将9维向量变换为2维输出,即正面情感和负面情感的预测。

Dive-into-DL-PyTorch
pytorch代码实现:
https://github.com/chenpaopao/TextCNN

总结:

  • 可以使用一维卷积来表征时序数据。
  • 多输入通道的一维互相关运算可以看作单输入通道的二维互相关运算。
  • 时序最大池化层的输入在各个通道上的时间步数可以不同。
  • textCNN主要使用了一维卷积层和时序最大池化层。
https://www.pexels.com/zh-cn/photo/977739/

数据扩充和增广

chenpaopao

最近在学习 torch,对于图像数据的预处理, torchvision 提供了torchvision.transforms 模块,用于预处理。

  1. 1. 裁剪——Crop 中心裁剪:transforms.CenterCrop 随机裁剪:transforms.RandomCrop 随机长宽比裁剪:transforms.RandomResizedCrop 上下左右中心裁剪:transforms.FiveCrop 上下左右中心裁剪后翻转,transforms.TenCrop
  2. 2. 翻转和旋转——Flip and Rotation 依概率p水平翻转:transforms.RandomHorizontalFlip(p=0.5) 依概率p垂直翻转:transforms.RandomVerticalFlip(p=0.5) 随机旋转:transforms.RandomRotation
  3. 3. 图像变换 resize:transforms.Resize 标准化:transforms.Normalize 转为tensor,并归一化至[0-1]:transforms.ToTensor 填充:transforms.Pad 修改亮度、对比度和饱和度:transforms.ColorJitter 转灰度图:transforms.Grayscale 线性变换:transforms.LinearTransformation() 仿射变换:transforms.RandomAffine 依概率p转为灰度图:transforms.RandomGrayscale 将数据转换为PILImage:transforms.ToPILImage transforms.Lambda:Apply a user-defined lambda as a transform.
  4. 4. 对transforms操作,使数据增强更灵活 transforms.RandomChoice(transforms), 从给定的一系列transforms中选一个进行操作 transforms.RandomApply(transforms, p=0.5),给一个transform加上概率,依概率进行操作 transforms.RandomOrder,将transforms中的操作随机打乱

此外,还提供了 torchvision.transforms.Compose( ),可以同时传递多个函数

mytransform = transforms.Compose([
transforms.ToTensor()
]
)

# torch.utils.data.DataLoader
cifarSet = torchvision.datasets.CIFAR10(root = "../data/cifar/", train= True, download = True, transform = mytransform )
cifarLoader = torch.utils.data.DataLoader(cifarSet, batch_size= 10, shuffle= False, num_workers= 2)
>>> transforms.Compose([ 
>>> transforms.CenterCrop(10),
>>> transforms.PILToTensor(), >>> transforms.ConvertImageDtype(torch.float), >>> ])

作为 Dataset类的参数传递 :

torchvision.datasets.Caltech101(root: strtarget_type: Union[List[str], str] = ‘category’transform: Optional[Callable] = Nonetarget_transform: Optional[Callable] = Nonedownload: bool = False)

或者自定义的类:
(自己实现torchvision.datasets.CIFAR10的功能)

(自己实现torchvision.datasets.CIFAR10的功能)
import os
import torch
import torch.utils.data as data
from PIL import Image

def default_loader(path):
return Image.open(path).convert('RGB')

class myImageFloder(data.Dataset):
def __init__(self, root, label, transform = None, target_transform=None, loader=default_loader):
fh = open(label)
c=0
imgs=[]
class_names=[]
for line in fh.readlines():
if c==0:
class_names=[n.strip() for n in line.rstrip().split('    ')]
else:
cls = line.split()
fn = cls.pop(0)
if os.path.isfile(os.path.join(root, fn)):
imgs.append((fn, tuple([float(v) for v in cls])))
c=c+1
self.root = root
self.imgs = imgs
self.classes = class_names
self.transform = transform
self.target_transform = target_transform
self.loader = loader

def __getitem__(self, index):
fn, label = self.imgs[index]
img = self.loader(os.path.join(self.root, fn))
if self.transform is not None:
img = self.transform(img)
return img, torch.Tensor(label)

def __len__(self):
return len(self.imgs)
def getName(self):
return self.classes

实例化torch.utils.data.DataLoader

mytransform = transforms.Compose([
transforms.ToTensor()
]
)

# torch.utils.data.DataLoader
imgLoader = torch.utils.data.DataLoader(
myFloder.myImageFloder(root = "../data/testImages/images", label = "../data/testImages/test_images.txt", transform = mytransform ),
batch_size= 2, shuffle= False, num_workers= 2)

for i, data in enumerate(imgLoader, 0):
print(data[i][0])
# opencv
img2 = data[i][0].numpy()*255
img2 = img2.astype('uint8')
img2 = np.transpose(img2, (1,2,0))
img2=img2[:,:,::-1]#RGB->BGR
cv2.imshow('img2', img2)
cv2.waitKey()
break

2 使用Python+OpenCV进行数据扩充(适用于目标检测)

https://pythonmana.com/2021/12/202112131040182515.html

下面内容来自

数据扩充是一种增加数据集多样性的技术,无需收集更多真实数据,但仍有助于提高模型精度并防止模型过拟合。

数据扩充方法包括:

  1. 随机裁剪
  2. Cutout
  3. 颜色抖动
  4. 增加噪音
  5. 过滤
import os

import cv2

import numpy as np

import random


def file_lines_to_list(path):

    '''

    ### 在TXT文件里的行转换为列表 ###

    path: 文件路径

    '''

    with open(path) as f:

        content = f.readlines()

    content = [(x.strip()).split() for x in content]

    return content


def get_file_name(path):

    
'''

    ### 获取Filepath的文件名 ###

    path: 文件路径

    '''

    basename = os.path.basename(path)

    onlyname = os.path.splitext(basename)[0]

    return onlyname


def write_anno_to_txt(boxes, filepath):

    
'''

    ### 给TXT文件写注释 ###

    boxes: format [[obj x1 y1 x2 y2],...]

    filepath: 文件路径
    '''

    txt_file = open(filepath, "w")

    for box in boxes:

        print(box[0], int(box[1]), int(box[2]), int(box[3]), int(box[4]), file=txt_file)

    txt_file.close()

随机裁剪

随机裁剪随机选择一个区域并进行裁剪以生成新的数据样本,裁剪后的区域应具有与原始图像相同的宽高比,以保持对象的形状。

def randomcrop(img, gt_boxes, scale=0.5):

    
'''

    ### 随机裁剪 ###

    img: 图像

    gt_boxes: format [[obj x1 y1 x2 y2],...]

    scale: 裁剪区域百分比
    '''


    # 裁剪

    height, width = int(img.shape[0]*scale), int(img.shape[1]*scale)

    x = random.randint(0, img.shape[1] - int(width))

    y = random.randint(0, img.shape[0] - int(height))

    cropped = img[y:y+height, x:x+width]

    resized = cv2.resize(cropped, (img.shape[1], img.shape[0]))


    # 修改注释

    new_boxes=[]

    for box in gt_boxes:

        obj_name = box[0]

        x1 = int(box[1])

        y1 = int(box[2])

        x2 = int(box[3])

        y2 = int(box[4])

        x1, x2 = x1-x, x2-x

        y1, y2 = y1-y, y2-y

        x1, y1, x2, y2 = x1/scale, y1/scale, x2/scale, y2/scale

        if (x1<img.shape[1] and y1<img.shape[0]) and (x2>0 and y2>0):

            if x1<0: x1=0

            if y1<0: y1=0

            if x2>img.shape[1]: x2=img.shape[1]

            if y2>img.shape[0]: y2=img.shape[0]

            new_boxes.append([obj_name, x1, y1, x2, y2])

    return resized, new_boxes

Cutout

Terrance DeVries和Graham W.Taylor在2017年的论文中介绍了Cutout,它是一种简单的正则化技术,用于在训练过程中随机屏蔽输入的方块区域,可用于提高卷积神经网络的鲁棒性和整体性能。这种方法不仅非常容易实现,而且还表明它可以与现有形式的数据扩充和其他正则化工具结合使用,以进一步提高模型性能。如本文所述,剪切用于提高图像识别(分类)的准确性,因此,如果我们将相同的方案部署到对象检测数据集中,可能会导致丢失对象的问题,尤其是小对象。

剪切输出是新生成的图像,我们不移除对象或更改图像大小,则生成图像的注释与原始图像相同。

def cutout(img, gt_boxes, amount=0.5):

    
'''

    ### Cutout ###

    img: 图像

    gt_boxes: format [[obj x1 y1 x2 y2],...]

    amount: 蒙版数量/对象数量
    '''

    out = img.copy()

    ran_select = random.sample(gt_boxes, round(amount*len(gt_boxes)))


    for box in ran_select:

        x1 = int(box[1])

        y1 = int(box[2])

        x2 = int(box[3])

        y2 = int(box[4])

        mask_w = int((x2 - x1)*0.5)

        mask_h = int((y2 - y1)*0.5)

        mask_x1 = random.randint(x1, x2 - mask_w)

        mask_y1 = random.randint(y1, y2 - mask_h)

        mask_x2 = mask_x1 + mask_w

        mask_y2 = mask_y1 + mask_h

        cv2.rectangle(out, (mask_x1, mask_y1), (mask_x2, mask_y2), (0, 0, 0), thickness=-1)

    return out

颜色抖动

ColorJitter是另一种简单的图像数据增强,我们可以随机改变图像的亮度、对比度和饱和度。我相信这个技术很容易被大多数读者理解。

def colorjitter(img, cj_type="b"):

    
'''

    ### 不同的颜色抖动 ###

    img: 图像

    cj_type: {b: brightness, s: saturation, c: constast}
    '''

    if cj_type == "b":

        # value = random.randint(-50, 50)

        value = np.random.choice(np.array([-50, -40, -30, 30, 40, 50]))

        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

        h, s, v = cv2.split(hsv)

        if value >= 0:

            lim = 255 - value

            v[v > lim] = 255

            v[v <= lim] += value

        else:

            lim = np.absolute(value)

            v[v < lim] = 0

            v[v >= lim] -= np.absolute(value)


        final_hsv = cv2.merge((h, s, v))

        img = cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)

        return img


    elif cj_type == "s":

        # value = random.randint(-50, 50)

        value = np.random.choice(np.array([-50, -40, -30, 30, 40, 50]))

        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

        h, s, v = cv2.split(hsv)

        if value >= 0:

            lim = 255 - value

            s[s > lim] = 255

            s[s <= lim] += value

        else:

            lim = np.absolute(value)

            s[s < lim] = 0

            s[s >= lim] -= np.absolute(value)


        final_hsv = cv2.merge((h, s, v))

        img = cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)

        return img


    elif cj_type == "c":

        brightness = 10

        contrast = random.randint(40, 100)

        dummy = np.int16(img)

        dummy = dummy * (contrast/127+1) - contrast + brightness

        dummy = np.clip(dummy, 0, 255)

        img = np.uint8(dummy)

        return img

增加噪声

在一般意义上,噪声被认为是图像中的一个意外因素,然而,几种类型的噪声(例如高斯噪声、椒盐噪声)可用于数据增强,在深度学习中添加噪声是一种非常简单和有益的数据增强方法。

对于那些无法识别高斯噪声和椒盐噪声之间差异的人,高斯噪声的值范围为0到255,具体取决于配置,因此,在RGB图像中,高斯噪声像素可以是任何颜色。相比之下,椒盐噪波像素只能有两个值0或255,分别对应于黑色(PEPER)或白色(salt)。

def noisy(img, noise_type="gauss"):

    
'''

    ### 添加噪声 ###

    img: 图像

    cj_type: {gauss: gaussian, sp: salt & pepper}
    '''

    if noise_type == "gauss":

        image=img.copy() 

        mean=0

        st=0.7

        gauss = np.random.normal(mean,st,image.shape)

        gauss = gauss.astype('uint8')

        image = cv2.add(image,gauss)

        return image


    elif noise_type == "sp":

        image=img.copy() 

        prob = 0.05

        if len(image.shape) == 2:

            black = 0

            white = 255            

        else:

            colorspace = image.shape[2]

            if colorspace == 3:  # RGB

                black = np.array([0, 0, 0], dtype='uint8')

                white = np.array([255, 255, 255], dtype='uint8')

            else:  # RGBA

                black = np.array([0, 0, 0, 255], dtype='uint8')

                white = np.array([255, 255, 255, 255], dtype='uint8')

        probs = np.random.random(image.shape[:2])

        image[probs < (prob / 2)] = black

        image[probs > 1 - (prob / 2)] = white

        return image

滤波

本文介绍的最后一个数据扩充过程是滤波。与添加噪声类似,滤波也简单且易于实现。实现中使用的三种类型的滤波包括模糊(平均)、高斯和中值。

def filters(img, f_type = "blur"):

    
'''

    ### 滤波 ###

    img: 图像

    f_type: {blur: blur, gaussian: gaussian, median: median}
    '''

    if f_type == "blur":

        image=img.copy()

        fsize = 9

        return cv2.blur(image,(fsize,fsize))


    elif f_type == "gaussian":

        image=img.copy()

        fsize = 9

        return cv2.GaussianBlur(image, (fsize, fsize), 0)


    elif f_type == "median":

        image=img.copy()

        fsize = 9

        return cv2.medianBlur(image, fsize)

上述内容可以在这里找到完整实现

https://github.com/tranleanh/data-augmentation

机器学习-吴恩达

笔记地址

http://www.ai-start.com/ml2014/

github链接:(笔记实现)

https://github.com/fengdu78/Coursera-ML-AndrewNg-Notes  

github链接:(code实现)

https://github.com/fengdu78/Coursera-ML-AndrewNg-Notes/tree/master/code

建议看这个https://github.com/mstampfer/Coursera-Stanford-ML-Python

视频:

https://www.bilibili.com/video/BV164411b7dx?p=1

https://www.coursera.org/learn/machine-learning/home/welcome