非监督学习

聚类(Clustering)

聚类是一种非监督学习算法,可以自动寻找数据中的结构,将相似的数据自动归为一组。相比于分类算法,它不需要提供训练数据的真值 y 。聚类算法的应用有:

  • 合并同类新闻
  • 市场划分
  • DNA 分析
  • 天文数据分析

K均值聚类算法(K-mean algorithm)

K 均值聚类算法是聚类算法的一种。它会初始化 K 个聚类中心(cluster centroids),然后对所有数据重复下面两步操作:

  • 求每个数据到所有聚类中心的距离,找到距离最小的那个,把这个数据分配给这个聚类中心
  • 计算每个聚类中心的所有数据的均值,将这个均值指定为每个聚类中心新的位置。

重复上面两步直至聚类中心的位置不再变化。

有特殊情况是有些聚类中心没有分配到数据。常见的做法是去除这个聚类中心,或者也可以重新初始化这个聚类中心,期待它重新计算后可以分配到数据。

代价函数

第二步中我们会计算每个聚类中心与其被分配到的数据的距离,将这个距离平方,将所有距离的平方相加然后除以数据总数,我们就得到了 K 均值算法的代价函数。也被称为 Distortion 。上面一直重复的两步操作都在最小化这个代价函数。当代价停止减小时,我们就可以停止重复上面的两步并检查模型的拟合情况。

初始值选择

假设我们有 K 个聚类中心,一般的做法是从所有数据中随机选择 K 个数据,将它们作为 K 个聚类中心的初始值。但单次随机选择容易得到几个相同的聚类中心,达不到最好的效果。所以,我们会选择 50-1000 次 K 个初始值,然后对这么多个初始值分别应用聚类算法,但每个初始值只计算一次代价,并不重复上面提到的两步操作。然后,从所有这些代价中选择最小的那个作为我们真正的初始值来重复上面的两步。

K 值选择

选择一个合适的聚类中心的数量会让我们的 K 值聚类算法拟合的更好。但很多场景并没有一个明显的 K 值。一个方法是尝试多个不同的 K 值,画出 K 值和代价的曲线,找到拐点,也就是代价下降由快到慢的那个转折点的 K 值作为实际使用的 K 值。在有些情况下代价随着 K 值的增加是缓慢下降的,这样这个方法就无法确定 K 值了。

实际上我们训练 K聚类算法模型是为了后续的用途,所以,我们要根据后续实际用途的效果来权衡 K 值的选择。比如:我们训练一个 K 均值聚类算法模型来压缩图片,根据我们想要压缩后的图片是比较清晰还是比较小,我们可以需要不同的 K 值。

异常检测(Anomaly detection)

异常检测是不同于聚类的另一种算法,它用于检测异常的行为。比如:制造出的物品是否合格,用户在网站上的行为是否正常,是否被盗号。

实现异常检测算法的流程为:

  1. 选择 \( n \) 个与异常行为有关的特征 \( x \)
  2. 在每个特征上计算平均数和方差
  3. 在新的样本上根据平均数和方差根据高斯分布计算每个特征的概率并相乘得到一个概率
  4. 将得到的概率与一个阈值比较,小于阈值会判定为异常行为

总体来看,异常检测算法是在检测是否有一个或多个特征出现的概率很小,小就判定为异常。

模型评估和优化

当我们训练好一个异常检测算法模型后,我们怎么选择一个好的阈值呢?怎么去优化这个模型呢?这时候就需要进行模型的评估。虽然异常检测是非监督学习算法,但那是指训练的时候我们没有用到有标签的数据,在评估过程中我们可以找一些确认为异常的数据,将他们和部分正常数据混合成两部分:交叉验证集和测试集,这点和监督学习是一样的。我们可以在交叉验证集中评估我们的模型,根据结果调整这个阈值,然后将最终的模型在测试集上测试并得到我们模型最终的效果。

当我们用于评估的异常数据样本很少时,比如只有一两个,那我们可以部分成交叉验证集和测试集,而是将全部的异常样本和部分正常样本混合成交叉验证集,不要测试集。这也可以优化我们的模型,但有过拟合的风险。

异常检测与监督学习

之前提到,我们可以用一些有标签的数据来优化和评估我们的模型,那我们为什么不直接训练一个监督学习模型呢?🤔下面是异常检测和监督学习模式合适应用场景的比较:

异常检测

  • 当异常数据的量很少,正常数据的量很大时
  • 当异常情况可能会与我们拥有的样本完全不同时

监督学习模型

  • 当异常数据和正常数据的数量都很多时
  • 当未来出来的异常情况都与我们的样本相似时

造成这些不同适用条件的原因是:

  • 异常检测算法是计算数据是正常数据的概率,我们的模型在大量的正常数据样本上总结了经验,当新的数据与我们正常数据相差很多时,不管它是否出现在我们评估时用到的异常数据中,它都会被模型识别到出现的概率很小并判断为异常数据。比如诈骗,诈骗招数会与时俱进,我们的诈骗样本可能过几年就不适用了。
  • 监督学习模型是既总结正常数据的经验,又总结异常数据的经验。所以,当我们的异常数据很少或我们的异常数据样本并没有包含大多数的异常情况时,监督学习模型的效果会很差。当我们有足够多的异常和正常数据时,它会表现的很好。比如垃圾邮件,垃圾邮件虽然有很多种,但基本都是相同的情况,要卖给你东西或者诱导你点击网站。

特征选择

在监督学习中,模型有数据的标签作为真值,它可以过滤掉没用的特征或调整调整的参数来适应特征。而在非监督学习中,我们没有真值给模型训练,所以,选择合适的特征尤为重要。

高斯化

我们计算每个特征的概率时是根据高斯分布来计算的,加入我们原本特征的分布不是高斯分布,那我们计算的概率可能不会很好。所以,我们需要绘制每个特征的分布曲线,如果不是高斯分布,那就使用一些函数来将该特征转换为类似高斯分布的数据。比如使用 \( \log\left( x + C \right) \) ,取平方根,立方根等来将原来的数据转为新的数据。

错误分析

当我们使用了交叉验证集评估了模型后,我们可以查看模型预测错的例子,找出模型为什么预测错了,尝试一些办法,比如增加新的特征来使模型可以正确预测这种数据。

创建新的特征

有时,异常发生时,单个特征值会很大,但平常这个特征的值可能会和其他特征值一起变得很大,这时候就需要我们将几个特征结合起来组成新的特征。比如,正常服务器运行时 CPU 和网络 IO 都可能很大,当被黑客入侵时, CPU 负载可能会很大,网络 IO 不大。这时,我们就可以将 CPU 负载和网络 IO 结合起来组成新的特征:CPU负载/网络 IO。这样就可以区分这两种情况。

推荐系统

推荐系统在现实世界中的应用非常广泛,当我们访问例如淘宝,京东之类的网站,它们都会根据我们的喜好来推荐不同的商品。

协同过滤(Collaborative Filtering)

协同过滤算法的目的是生成两个向量,假设我们要针对电影来做推荐系统。那么我们生成的两个向量为:

  1. 对于每个用户,生成能够表现这个用户电影品味(喜欢程度)的向量
  2. 对于每个电影,生成能够代表这个电影特征的向量

这两个向量的点乘加上一个偏差值就代表我们的算法预测该用户会对该电影打多少分。

代价函数

$$J({\mathbf{x}^{(0)},...,\mathbf{x}^{(n_m-1)},\mathbf{w}^{(0)},b^{(0)},...,\mathbf{w}^{(n_u-1)},b^{(n_u-1)}})= \left[ \frac{1}{2}\sum_{(i,j):r(i,j)=1}(\mathbf{w}^{(j)} \cdot \mathbf{x}^{(i)} + b^{(j)} - y^{(i,j)})^2 \right] + \underbrace{\left[\frac{\lambda}{2}\sum_{j=0}^{n_u-1}\sum_{k=0}^{n-1}(\mathbf{w}^{(j)}_k)^2 + \frac{\lambda}{2}\sum_{i=0}^{n_m-1}\sum_{k=0}^{n-1}(\mathbf{x}_k^{(i)})^2\right]}_{regularization}$$

其实协同过滤的代价函数和线性回归差不多,主要的区别是变量不仅是 \(w\) 和 \(b\),还有 \(x\)。我们会同时训练 \(w\), \(x\), \(b\)

$$ w = w - \alpha \frac{\partial J(w,b,x)}{\partial w} $$

$$ b = b - \alpha \frac{\partial J(w,b,x)}{\partial b} $$

$$ x = x - \alpha \frac{\partial J(w,b,x)}{\partial x} $$

当我们的训练数据是二元的标签数据时,例如:喜欢/不喜欢。我们可以将 \(\mathbf{w}^{(j)} \cdot \mathbf{x}^{(i)} + b^{(j)}\) 通过 sigmod 函数处理,然后代价函数就变成了类似逻辑回归的代价函数。

均值归一化(Mean normalization)

当我们训练好协同过滤模型后,如果有一个新用户,他没有评价过任何一部电影,当我们初始的 \(b\) 为 0 时,那么我们的模型将会预测它会给所有电影打 0 分,这显然是不对的。我们可以用均值归一化来解决这个问题。

还是以电影评分为例。我们可以取每个电影的均分,然后将每个用户的评分减去这个均分。当我们计算这个用户对某个电影的评分时,我们需要再加上这个均分,也就是从 \(\mathbf{w}^{(j)} \cdot \mathbf{x}^{(i)} + b^{(j)}\) 改为 \(\mathbf{w}^{(j)} \cdot \mathbf{x}^{(i)} + b^{(j)} + u\) 。 \(u\) 指的就是这个均分。这样,当模型预测一个从没有评价过电影的新用户对某个电影的评分时,这个评分会时这个电影的平均评分,比之前的 0 合理。

TensorFlow 实现

我们需要使用 TensorFlow 的 Custom Training Loop 功能来实现我们的算法。Custom Training Loop 可以让我们子集自定义计算代价的公式,TensorFlow 会自动帮助我们根据公式求偏导数,这一特性叫做 Auto Diff 。示例代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
optimizer = keras.optimizers.Adam(learning_rate=1e-1)

iterations = 200
lambda_ = 1
for iter in range(iterations):
    # Use TensorFlow’s GradientTape
    # to record the operations used to compute the cost 
    with tf.GradientTape() as tape:

        # Compute the cost (forward pass included in cost)
        cost_value = cofi_cost_func_v(X, W, b, Ynorm, R, lambda_)

    # Use the gradient tape to automatically retrieve
    # the gradients of the trainable variables with respect to the loss
    grads = tape.gradient( cost_value, [X,W,b] )

    # Run one step of gradient descent by updating
    # the value of the variables to minimize the loss.
    optimizer.apply_gradients( zip(grads, [X,W,b]) )

找到相关的物品

当我们训练好协同过滤模型后,我们会得到每个物品的特征向量。当我们需要找到与某个物品相关的物品时,我们可以将所有的特征向量与该物品的的特征向量做差值并。找到差值最小的即可。

局限性

  • 冷启动问题:当有一个全新的,没有人评过分的物品时,协同过滤算法的效果不好。当用户评过分的物品较少时,协同过滤算法的效果也不好。
  • 对附带信息的使用不是很好:例如用户的性别,年龄,位置。物品的类别,流派等。

内容过滤(Content-based filtering)

相比于协同过滤,我们有用户的特征(年龄,性别等)和物品的特征(流派,平均评分等)。我们想基于这些特征来给用户推荐合适的物品。我们要做的就是计算用户的特征向量 v_u 和物品的特征向量 v_m ,并将这两个向量点乘来判断该物品和用户是否合适。

理论实现

我们可以构建两个神经网络:用户神经网络和物品神经网络来分别从用户和物品的特征中生成对应的特征向量。因为我们最终要点乘这两个神经网络的输出,所以这两个神经网络拥有相同结构的输出层,隐藏层可以不同。然后将这两个神经网络合并成一个来作为我们最终的神经网络。当我们想要的结果是评估用户是否想要这个物品时,我们可以将点乘的结果套上一个 sigmod 函数来使其输出 0/1 。

性能优化

内容过滤推荐系统的运行方式可以分为两步:检索和排序

  1. 检索:检索的功能是生成一个用户可能会喜欢的物品的列表。例如:

    • 查看用户最新浏览的十个物品,找到十个相似的物品加入列表
    • 统计用户浏览过的物品,找到出现率最高的三个类别,将这些类别的 top 10 物品加入列表
    • 将用户所属国家的 top 30 加入列表

    将上述所有步骤生成的列表整合到一个列表中,去除重复的和用户已经有的。

  2. 排序:将检索生成的列表中的所有物品和用户特征放入神经网络进行推理,得到用户对这些物品的可能喜爱程度并按照从大到小的顺序进行排序再展示给用户。

这之中存在一个权衡,当我们检索功能输出的列表很大时,我们根据这个列表排序就会很慢。当我们降低检索功能输出的列表大小时,我们又担心检索范围太小,无法检索到用户真正喜欢的物品。所以,我们需要进行离线测试,尝试不同的检索功能输出的列表大小来找到一个满足我们需求的点。

道德问题

构建推荐系统时,应考虑在道德方面的影响。尽量构建符合道德的,对社会和人类有益的系统。

主成分分析(Principal Component Analysis)

当我们有一个比较复杂的数据集,它拥有十几个或者成百上千个特征时,我们很难去可视化这样的数据集。主成分分析可以帮助我们将大量的特征转化为两个或三个特征来方便我们可视化数据。比如:它可以删除变化不大的特征,将某几个相关特征整合成一个等。这样,我们就可以通过可视化更好的理解数据。

具体做法

假设我们有一组数据,x1 作为横轴, x2 作为纵轴。如果他们的取值范围相差很大,需要先进行特征缩放。接下来,PCA 算法会尝试找到一个新的轴,从每个点到这个新的轴作垂线,相交的点就看作该点在这个新的轴上的映射。然后计算所有点在这个轴上的方差,这时的方差可以看作保留原本信息的程度,越大越好。重复上述步骤,找到方差最大的那个轴作为原来的 x1 和 x2 的替代。比如一个有一个点(2, 3)。原来的 x1, x2 两个轴的(1,1)在新的轴上的坐标为(0.7, 0.7)那么这个点(2, 3)在新的轴上的坐标就是 2 * 0.7 + 3 * 0.7 = 3.5 。这样确定好第一个轴后,第二个轴就是跟第一个轴垂直的那条直线,第三个轴就是和第一,第二轴组成的平面垂直的那条直线。

与线性回归的区别

看起来主成分分析的做法与线性回归差不多,都是拟合一条直线。但它们还是有区别的。

  • 线性回归计算的是每个点的 y 与拟合的直线的距离,这个距离是从该点做一条垂直于 x 轴的垂线与拟合的直线相交,取这条线段的距离。线性回归使用 y 的距离作为调整拟合的直线的依据。
  • 主成分分析是从 x1, x2 这个点做一条垂直于拟合的轴的垂线,取这个线段作为距离。它是使用 x1, x2 两个特征作为调整轴的依据的。

反推原来点的坐标

在我们使用新的轴替代了老的两个轴后,我们已经不可能通过新的轴上的点的坐标反推出旧的点的精确坐标的。但我们可以得到一个近似值,将新的坐标轴的点乘上原来坐标轴的(1,1)点在新的轴上所对应的坐标就行。比如 3.5 = (3.5 * 0.7, 3.5 * 0.7) = (2.45, 2.45)。相较于原来的(2,3)可以看作一个近似值。

强化学习

当我们想要使用代码自动控制一个机器人或者飞行器时,我们有这个物体的状态(坐标,动力大小,方向等),想要训练一个监督学习模型来根据这些状态来自动决定下一步干什么是很困难的,因为正确的下一步动作是模棱两可的,你可以加油门,也可以调整倾角。这时,我们需要强化学习来解决这个事。

强化学习的关键是奖励函数(reward function) ,这个函数可以判断算法模型的输出是否是好的,满足我们期望的输出。如果是,那就奖励算法,如果不是,那就惩罚算法。通过这个奖励函数来让算法自己发现怎么做才能取得好的结果。强化学习与其他算法的区别就在这里,我们不会教算法怎么去取得好的结果,而是评判它每次做的好不好从而让算法自己找到方法去取得好的结果。有点像训练狗狗,当狗狗表现的好时,我们就奖励狗狗吃的或者夸它,当狗狗表现的不好时,我们就批评它。通过奖惩来训练它称为满足我们要求的好狗狗🐶。

为了实现这个奖励机制,我们需要计算模型每个动作能够获得的回报(return),这个回报可以为正数,零或者负数。每进行一个动作,我们会将该动作获得的奖励乘上一个折扣系数(discount factor)的 n 次方,n 为步数。比如:一开始的奖励为 0 ,我们第一个动作做完后,本应获得的奖励为 10 ,我们设定的折扣系数为 0.9 。那么实际获得的奖励就是 10 * 0.9 = 9 。接下来我们执行第二个动作,第二个动作本来给的奖励为 20 ,乘上折扣系数,实际的奖励为 20 * (0.9 ** 2) = 16.2 。假设第二个动作完成后我们达到了结束状态(terminal state)。那么,我们总共获得的奖励为 0 + 9 + 16.2 = 25.2 。

我们还需要一个策略(policy)\(\pi\) 来将状态与实际行动通过上面提到的奖励函数结合起来。我们给这个策略一个确定的状态,策略可以根据这个状态计算接下来的回报,比较不同的回报来输出一个行动,然后实际执行这个行动。\(action = \pi(state) \)

总结一下,我们上面提到的所有步骤整合起来就是强化学习的运作形式,这个形式有一个名字:马尔可夫决策过程(Markov Decision Process) ,简称 MDP 。马尔可夫指的是:未来的状态仅取决于现在的状态,与当前状态的之前的所有事都无关。

状态价值函数(State-action value function)(Q-function)

状态价值函数,简称 Q 函数,这个函数用于帮助我们计算当前状态 s 时,我们采取行动 a 能取得的回报是多少。通过在当前状态尝试多种不同的 a ,取其中回报最大的 a 来当作我们当前状态的 \( \pi(state)\) 所输出的 a 。

贝尔曼方程(Bellman Equation)

$$ Q(s, a) = R(s) + \gamma \max_{a’} Q(s’, a’) $$

上面就是贝尔曼方程,它就是我们用来实现状态价值函数的方法。其中,\(R(s)\) 是当前状态的回报,\(\gamma\) 是折扣系数,\(s’\) 是我们执行完动作 a 后进入到的下一个状态,\(a’\) 是 \(s’\) 状态所能执行的动作。方程等号的右边可以分为两部分:

  • \(R(s)\) 代表即时奖励
  • \( \gamma \max_{a’} Q(s’, a’)\) 代表后续获得的奖励

神经网络架构

我们可以使用神经网络来实现对函数 \(Q(s, a)\) 的模拟。神经网络的输入为当前的状态 s ,由多个参数构成,例如x, y, v_x, x_y 等。输出层的神经元个数等同于所有的动作个数。假设我们对于每个状态都有三个动作可以选择:向左,向右,什么也不做。那么我们要构建的神经网络的输出层的神经元个数就是 3 。这样可以一次推理就计算出所有可能动作的回报,使我们的计算更有效率。

完整的过程为:

  • 随机初始化神经网络的参数
  • 通过实验收集各种各样的所需数据:当前状态 s,选择的动作 a,当前回报 R(s),执行完动作所进入到的下一个状态 s’。存储最新的大概 10000 个数据
  • 然后将每组数据的 s 作为 x ,\(R(s) + \gamma \max_{a’} Q(s’, a’)\) 作为 y 来训练神经网络,使用新的神经网络替换原有神经网络
  • 重复收集数据和训练神经网络的步骤

在学习过程中选择合适的动作

上面提到我们要收集训练数据来训练我们的神经网络,如果我们全部随机选择下一步动作,训练的效果不会很好,而我们又不知道最合适的动作是什么。我们该如何选择下一步执行的动作呢?常见的做法是:

  • 大概率(比如:95%)的情况下,选择能够最大化 \(Q(s’, a’)\) 的动作
  • 小概率(比如:5%)的情况下,选择一个随机的动作

这种方法叫做:\(\epsilon\)-greedy policy 。\(\epsilon\) 指的是选择随机动作的概率,在这里是 5%。还有一个常见的技巧是:开始时选择比较大的 \(\epsilon\) ,随着训练的进行减小 \(\epsilon\) 。

优化

小批量学习(Mini-batch)

当我们 的训练数据量很大时,每一次我们的梯度下降都会求所有训练数据的导数,这会导致我们的训练进行的很慢。为了加快训练,我们可以每次训练不使用全部的数据,而是使用其中一部分数据。比如我们总共有一亿个训练数据,每次训练时,我们都使用不同的 1000 组数据来训练。这样可以加快我们的训练速度。当然,相比于使用全部数据来训练,这样会使我们的梯度下降变得不是很稳定,偶尔会朝着最小梯度以外的方向下降,但总体上还是朝着正确的方向下降。

软更新(Soft Update)

之前提到的训练过程中会使用新训练好的神经网络来直接替换掉原来的神经网络,但有时我们新训练好的神经网络可能还不如原来的那个,这时替换的话会导致我们的拟合的更差。为了应对这种情况,我们可以采用软更新的方法:假设我们神经网络的参数是 W, B 那么,我们每次更新参数时不是直接使用 new_W , new_B 替换 W, B 。而是 W = 0.99W + 0.01new_W, B = 0.99B + 0.01new_B 。0.99 和 0.01 是可调的两个参数,要求相加等于 1 。这样可以使我们的神经网络拟合的更好。