22.11. 信息论
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

世界充满了信息。信息提供了一种跨越学科鸿沟的通用语言:从莎士比亚的十四行诗到康奈尔大学ArXiv上研究人员的论文,从梵高的画作《星夜》到贝多芬的第五交响曲,从第一个编程语言Plankalkül到最先进的机器学习算法。无论格式如何,一切都必须遵循信息论的规则。通过信息论,我们可以测量和比较不同信号中存在多少信息。在本节中,我们将探讨信息论的基本概念以及信息论在机器学习中的应用。

在我们开始之前,让我们先概述一下机器学习和信息论之间的关系。机器学习旨在从数据中提取有趣的信号并做出关键预测。另一方面,信息论研究信息的编码、解码、传输和处理。因此,信息论为讨论机器学习系统中的信息处理提供了基础语言。例如,许多机器学习应用使用交叉熵损失,如 第 4.1 节中所述。这种损失可以直接从信息论的考量中推导出来。

22.11.1. 信息

让我们从信息论的“灵魂”:信息开始。信息 可以被编码在任何具有一种或多种编码格式特定序列的事物中。假设我们的任务是尝试定义一个信息的概念。我们的出发点会是什么呢?

考虑以下思想实验。我们有一个朋友,他有一副扑克牌。他会洗牌,翻开一些牌,然后告诉我们关于这些牌的陈述。我们将尝试评估每个陈述的信息量。

首先,他翻开一张牌,告诉我们:“我看到一张牌。” 这完全没有给我们任何信息。我们本来就确定这是事实,所以我们希望信息量应该为零。

接下来,他翻开一张牌说:“我看到一张红心。” 这给我们提供了一些信息,但实际上只有 \(4\) 种可能的花色,每种花色出现的可能性都相同,所以我们对这个结果并不感到惊讶。我们希望无论信息的度量标准是什么,这个事件的信息量都应该很低。

接着,他翻开一张牌说:“这是黑桃 \(3\)。” 这提供了更多的信息。实际上有 \(52\) 种同样可能的 outcomes,而我们的朋友告诉了我们是哪一种。这应该算是中等量的信息。

让我们将这个实验推向逻辑的极致。假设最后他翻开了整副牌,并念出了洗牌后的完整序列。一副牌有 \(52!\) 种不同的顺序,同样都是等可能的,所以我们需要大量的信息才能知道是哪一种。

我们开发的任何信息概念都必须符合这种直觉。确实,在接下来的几节中,我们将学习如何计算出这些事件分别包含 \(0\textrm{ 比特}\)\(2\textrm{ 比特}\)\(~5.7\textrm{ 比特}\)\(~225.6\textrm{ 比特}\) 的信息。

如果我们仔细思考这些思想实验,会发现一个自然的想法。作为起点,与其关心知识本身,我们不如从信息代表事件的意外程度或抽象可能性的想法出发。例如,如果我们想描述一个不寻常的事件,我们需要大量的信息。对于一个常见的事件,我们可能不需要太多信息。

1948年,克劳德·E·香农发表了《通信的数学理论》(Shannon, 1948),建立了信息论。在他的文章中,香农首次引入了信息熵的概念。我们将从这里开始我们的旅程。

22.11.1.1. 自信息

由于信息体现了事件的抽象可能性,我们如何将可能性映射为比特数?香农引入了“比特”(bit)作为信息单位的术语,这个词最初由约翰·图基创造。那么,“比特”是什么?为什么我们用它来度量信息?历史上,一台老式发射机只能发送或接收两种类型的代码:\(0\)\(1\)。实际上,二进制编码在所有现代数字计算机中仍然普遍使用。通过这种方式,任何信息都由一系列的 \(0\)\(1\) 编码。因此,一个长度为 \(n\) 的二进制数字序列包含 \(n\) 比特的信息。

现在,假设对于任何代码序列,每个 \(0\)\(1\) 出现的概率为 \(\frac{1}{2}\)。因此,一个长度为 \(n\) 的代码序列事件 \(X\),发生的概率为 \(\frac{1}{2^n}\)。同时,如我们前面提到的,这个序列包含 \(n\) 比特的信息。那么,我们是否可以将其推广为一个数学函数,将概率 \(p\) 转换为比特数?香农通过定义*自信息*给出了答案

(22.11.1)\[I(X) = - \log_2 (p),\]

作为我们为这个事件 \(X\) 接收到的信息*比特*数。请注意,本节中我们总是使用以2为底的对数。为简单起见,本节的其余部分将省略对数表示法中的下标2,即 \(\log(.)\) 总是指 \(\log_2(.)\)。例如,代码“0010”的自信息为

(22.11.2)\[I(\textrm{"0010"}) = - \log (p(\textrm{"0010"})) = - \log \left( \frac{1}{2^4} \right) = 4 \textrm{ 比特}.\]

我们可以如下所示计算自信息。在此之前,让我们先导入本节中所有必要的包。

import torch
from torch.nn import NLLLoss


def nansum(x):
    # Define nansum, as pytorch does not offer it inbuilt.
    return x[~torch.isnan(x)].sum()

def self_information(p):
    return -torch.log2(torch.tensor(p)).item()

self_information(1 / 64)
6.0
import random
from mxnet import np
from mxnet.metric import NegativeLogLikelihood
from mxnet.ndarray import nansum


def self_information(p):
    return -np.log2(p)

self_information(1 / 64)
6.0
import tensorflow as tf


def log2(x):
    return tf.math.log(x) / tf.math.log(2.)

def nansum(x):
    return tf.reduce_sum(tf.where(tf.math.is_nan(
        x), tf.zeros_like(x), x), axis=-1)

def self_information(p):
    return -log2(tf.constant(p)).numpy()

self_information(1 / 64)
6.0

22.11.2.

由于自信息只度量单个离散事件的信息,我们需要一个更通用的度量,用于任何离散或连续分布的随机变量。

22.11.2.1. 熵的动机

让我们试着具体说明我们想要什么。这将是对所谓的*香农熵公理*的非正式陈述。事实证明,以下一系列常识性的陈述迫使我们得到一个唯一的信息定义。这些公理的正式版本以及其他几个版本可以在Csiszár (2008)中找到。

  1. 我们通过观察一个随机变量获得的信息,不依赖于我们如何称呼这些元素,也不依赖于概率为零的额外元素的存在。

  2. 我们通过观察两个随机变量获得的信息,不多于分别观察它们所获得的信息之和。如果它们是独立的,那么信息量恰好是和。

  3. 观察到(几乎)确定事件所获得的信息是(几乎)零。

虽然证明这个事实超出了我们文本的范围,但重要的是要知道,这唯一地决定了熵必须采取的形式。这些公理允许的唯一模糊性在于基本单位的选择,这通常通过我们之前看到的选择来规范化,即一次公平硬币翻转提供的信息是一比特。

22.11.2.2. 定义

对于任何服从概率分布 \(P\) 且具有概率密度函数(p.d.f.)或概率质量函数(p.m.f.) \(p(x)\) 的随机变量 \(X\),我们通过*熵*(或*香农熵*)来衡量信息的期望量

(22.11.3)\[H(X) = - E_{x \sim P} [\log p(x)].\]

具体来说,如果 \(X\) 是离散的,

(22.11.4)\[H(X) = - \sum_i p_i \log p_i \textrm{, 其中 } p_i = P(X_i).\]

否则,如果 \(X\) 是连续的,我们也称熵为*微分熵*

(22.11.5)\[H(X) = - \int_x p(x) \log p(x) \; dx.\]

我们可以如下定义熵。

def entropy(p):
    entropy = - p * torch.log2(p)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(entropy)
    return out

entropy(torch.tensor([0.1, 0.5, 0.1, 0.3]))
tensor(1.6855)
def entropy(p):
    entropy = - p * np.log2(p)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(entropy.as_nd_ndarray())
    return out

entropy(np.array([0.1, 0.5, 0.1, 0.3]))
[21:59:56] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
[1.6854753]
<NDArray 1 @cpu(0)>
def entropy(p):
    return nansum(- p * log2(p))

entropy(tf.constant([0.1, 0.5, 0.1, 0.3]))
<tf.Tensor: shape=(), dtype=float32, numpy=1.6854753>

22.11.2.3. 解释

你可能好奇:在熵的定义 (22.11.3) 中,为什么我们使用负对数的期望?这里有一些直观的解释。

首先,为什么我们使用*对数*函数 \(\log\)?假设 \(p(x) = f_1(x) f_2(x) \ldots, f_n(x)\),其中每个分量函数 \(f_i(x)\) 彼此独立。这意味着每个 \(f_i(x)\) 对从 \(p(x)\) 中获得的总信息独立贡献。如上所述,我们希望熵公式对于独立随机变量是可加的。幸运的是,\(\log\) 可以自然地将概率分布的乘积转换为各个项的和。

其次,为什么我们使用*负*\(\log\)?直观上,更频繁的事件应该包含更少的信息,而较不常见的事件则包含更多信息,因为我们通常从一个不寻常的案例中获得比从一个普通案例中更多的信息。然而,\(\log\) 随概率单调递增,并且对于 \([0, 1]\) 中的所有值实际上是负的。我们需要构建一个事件概率与其熵之间的单调递减关系,理想情况下这个关系总是正的(因为我们观察到的任何东西都不应该迫使我们忘记我们已经知道的)。因此,我们在 \(\log\) 函数前加上一个负号。

最后,*期望*函数从何而来?考虑一个随机变量 \(X\)。我们可以将自信息(\(-\log(p)\))解释为看到某个特定结果时的*意外*程度。确实,当概率趋近于零时,意外程度变为无穷大。类似地,我们可以将熵解释为观察 \(X\) 的平均意外程度。例如,想象一个老虎机系统以概率 \({p_1, \ldots, p_k}\) 分别发出统计上独立的符号 \({s_1, \ldots, s_k}\)。那么该系统的熵等于观察到每个输出的平均自信息,即:

(22.11.6)\[H(S) = \sum_i {p_i \cdot I(s_i)} = - \sum_i {p_i \cdot \log p_i}.\]

22.11.2.4. 熵的性质

通过上述例子和解释,我们可以推导出熵 (22.11.3) 的以下性质。在这里,我们将 \(X\) 称为一个事件,\(P\)\(X\) 的概率分布。

  • 对于所有离散的\(X\)\(H(X) \geq 0\)(对于连续的\(X\),熵可以为负)。

  • 如果 \(X \sim P\),其 p.d.f. 或 p.m.f. 为 \(p(x)\),我们尝试用一个新的概率分布 \(Q\)(其 p.d.f. 或 p.m.f. 为 \(q(x)\))来估计 \(P\),则

    (22.11.7)\[H(X) = - E_{x \sim P} [\log p(x)] \leq - E_{x \sim P} [\log q(x)], \textrm{ 当且仅当 } P = Q \textrm{ 时等号成立}.\]

    换句话说,\(H(X)\) 给出了编码从 \(P\) 中抽取的符号所需的平均比特数的下界。

  • 如果 \(X \sim P\),那么当 \(x\) 在所有可能的结果中均匀分布时,它传递的信息量最大。具体来说,如果概率分布 \(P\) 是离散的,有 \(k\)\(\{p_1, \ldots, p_k \}\),则

    (22.11.8)\[H(X) \leq \log(k), \textrm{ 当且仅当 } p_i = \frac{1}{k}, \forall i \textrm{ 时等号成立}.\]

    如果 \(P\) 是一个连续随机变量,那么情况会变得复杂得多。然而,如果我们额外假设 \(P\) 的支撑集是一个有限区间(所有值都在 \(0\)\(1\) 之间),那么当 \(P\) 是该区间上的均匀分布时,它的熵最高。

22.11.3. 互信息

前面我们定义了单个随机变量 \(X\) 的熵,那么一对随机变量 \((X, Y)\) 的熵又如何呢?我们可以认为这些技术试图回答以下这类问题:“与单独比较 \(X\)\(Y\) 相比,\(X\)\(Y\) 共同包含了哪些信息?是否存在冗余信息,还是所有信息都是独特的?”

在接下来的讨论中,我们总是使用 \((X, Y)\) 作为一对随机变量,它们遵循一个联合概率分布 \(P\),其概率密度函数(p.d.f.)或概率质量函数(p.m.f.)为 \(p_{X, Y}(x, y)\),而 \(X\)\(Y\) 分别遵循概率分布 \(p_X(x)\)\(p_Y(y)\)

22.11.3.1. 联合熵

类似于单个随机变量的熵 (22.11.3),我们定义一对随机变量 \((X, Y)\) 的*联合熵* \(H(X, Y)\)

(22.11.9)\[H(X, Y) = -E_{(x, y) \sim P} [\log p_{X, Y}(x, y)].\]

准确地说,一方面,如果 \((X, Y)\) 是一对离散随机变量,那么

(22.11.10)\[H(X, Y) = - \sum_{x} \sum_{y} p_{X, Y}(x, y) \log p_{X, Y}(x, y).\]

另一方面,如果 \((X, Y)\) 是一对连续随机变量,那么我们定义*微分联合熵*为

(22.11.11)\[H(X, Y) = - \int_{x, y} p_{X, Y}(x, y) \ \log p_{X, Y}(x, y) \;dx \;dy.\]

我们可以将 (22.11.9) 看作是告诉我们这对随机变量中的总随机性。作为两个极端情况,如果 \(X = Y\) 是两个相同的随机变量,那么这对变量中的信息恰好是其中一个的信息,我们有 \(H(X, Y) = H(X) = H(Y)\)。在另一个极端情况下,如果 \(X\)\(Y\) 是独立的,那么 \(H(X, Y) = H(X) + H(Y)\)。实际上,我们总是有:一对随机变量中包含的信息不小于任一随机变量的熵,也不超过两者之和。

(22.11.12)\[H(X), H(Y) \le H(X, Y) \le H(X) + H(Y).\]

让我们从头开始实现联合熵。

def joint_entropy(p_xy):
    joint_ent = -p_xy * torch.log2(p_xy)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(joint_ent)
    return out

joint_entropy(torch.tensor([[0.1, 0.5], [0.1, 0.3]]))
tensor(1.6855)
def joint_entropy(p_xy):
    joint_ent = -p_xy * np.log2(p_xy)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(joint_ent.as_nd_ndarray())
    return out

joint_entropy(np.array([[0.1, 0.5], [0.1, 0.3]]))
[1.6854753]
<NDArray 1 @cpu(0)>
def joint_entropy(p_xy):
    joint_ent = -p_xy * log2(p_xy)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(joint_ent)
    return out

joint_entropy(tf.constant([[0.1, 0.5], [0.1, 0.3]]))
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([0.8321928, 0.8532826], dtype=float32)>

注意,这和之前的*代码*是相同的,但现在我们将其解释为作用于两个随机变量的联合分布。

22.11.3.2. 条件熵

上面定义的联合熵是一对随机变量中包含的信息量。这很有用,但通常不是我们关心的。考虑机器学习的场景。让 \(X\) 作为描述图像像素值的随机变量(或随机变量向量),\(Y\) 作为类别标签的随机变量。\(X\) 应该包含大量信息——一幅自然图像是复杂的事物。然而,一旦图像被展示出来,\(Y\) 中包含的信息应该很少。实际上,一个数字的图像应该已经包含了它是哪个数字的信息,除非这个数字难以辨认。因此,为了继续扩展我们的信息论词汇,我们需要能够推断一个随机变量在另一个随机变量条件下的信息内容。

在概率论中,我们看到了*条件概率*的定义,用以衡量变量之间的关系。我们现在想类似地定义*条件熵* \(H(Y \mid X)\)。我们可以这样写:

(22.11.13)\[H(Y \mid X) = - E_{(x, y) \sim P} [\log p(y \mid x)],\]

其中 \(p(y \mid x) = \frac{p_{X, Y}(x, y)}{p_X(x)}\) 是条件概率。具体来说,如果 \((X, Y)\) 是一对离散随机变量,那么

(22.11.14)\[H(Y \mid X) = - \sum_{x} \sum_{y} p(x, y) \log p(y \mid x).\]

如果 \((X, Y)\) 是一对连续随机变量,那么*微分条件熵*的定义类似:

(22.11.15)\[H(Y \mid X) = - \int_x \int_y p(x, y) \ \log p(y \mid x) \;dx \;dy.\]

现在很自然地会问,*条件熵* \(H(Y \mid X)\) 与熵 \(H(X)\) 和联合熵 \(H(X, Y)\) 有什么关系?使用上面的定义,我们可以清晰地表达出来:

(22.11.16)\[H(Y \mid X) = H(X, Y) - H(X).\]

这有一个直观的解释:在给定 \(X\) 的情况下 \(Y\) 中的信息(\(H(Y \mid X)\))等于 \(X\)\(Y\) 共同包含的信息(\(H(X, Y)\))减去已经包含在 \(X\) 中的信息。这给了我们 \(Y\) 中那些没有在 \(X\) 中表示的信息。

现在,让我们从头开始实现条件熵 (22.11.13)

def conditional_entropy(p_xy, p_x):
    p_y_given_x = p_xy/p_x
    cond_ent = -p_xy * torch.log2(p_y_given_x)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(cond_ent)
    return out

conditional_entropy(torch.tensor([[0.1, 0.5], [0.2, 0.3]]),
                    torch.tensor([0.2, 0.8]))
tensor(0.8635)
def conditional_entropy(p_xy, p_x):
    p_y_given_x = p_xy/p_x
    cond_ent = -p_xy * np.log2(p_y_given_x)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(cond_ent.as_nd_ndarray())
    return out

conditional_entropy(np.array([[0.1, 0.5], [0.2, 0.3]]), np.array([0.2, 0.8]))
[0.8635472]
<NDArray 1 @cpu(0)>
def conditional_entropy(p_xy, p_x):
    p_y_given_x = p_xy/p_x
    cond_ent = -p_xy * log2(p_y_given_x)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(cond_ent)
    return out

conditional_entropy(tf.constant([[0.1, 0.5], [0.2, 0.3]]),
                    tf.constant([0.2, 0.8]))
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([0.43903595, 0.42451128], dtype=float32)>

22.11.3.3. 互信息

在之前随机变量 \((X, Y)\) 的设定下,你可能会想:“既然我们知道 \(Y\) 中包含但 \(X\) 中不包含的信息量,那么我们是否可以类似地问,\(X\)\(Y\) 之间共享了多少信息?”答案将是 \((X, Y)\) 的*互信息*,我们将其写作 \(I(X, Y)\)

与其直接深入到正式定义,不如先凭直觉练习一下,尝试完全基于我们之前构建的术语来推导互信息的表达式。我们希望找到两个随机变量之间共享的信息。我们可以尝试这样做:从 \(X\)\(Y\) 共同包含的所有信息开始,然后减去不共享的部分。\(X\)\(Y\) 共同包含的信息写为 \(H(X, Y)\)。我们想从中减去 \(X\) 中包含但 \(Y\) 中不包含的信息,以及 \(Y\) 中包含但 \(X\) 中不包含的信息。正如我们在上一节中看到的,这分别由 \(H(X \mid Y)\)\(H(Y \mid X)\) 给出。因此,我们得到互信息应该为:

(22.11.17)\[I(X, Y) = H(X, Y) - H(Y \mid X) - H(X \mid Y).\]

确实,这是一个互信息的有效定义。如果我们展开这些项的定义并将它们组合起来,经过一些代数运算,会发现这与下面的表达式相同:

(22.11.18)\[I(X, Y) = E_{x} E_{y} \left\{ p_{X, Y}(x, y) \log\frac{p_{X, Y}(x, y)}{p_X(x) p_Y(y)} \right\}.\]

我们可以在图 图 22.11.1 中总结所有这些关系。通过检验以下陈述是否也等价于 \(I(X, Y)\),可以很好地测试直觉。

  • \(H(X) - H(X \mid Y)\)

  • \(H(Y) - H(Y \mid X)\)

  • \(H(X) + H(Y) - H(X, Y)\)

../_images/mutual-information.svg

图 22.11.1 互信息与联合熵和条件熵的关系。

在许多方面,我们可以将互信息 (22.11.18) 视为我们在 第 22.6 节 中看到的相关系数的原则性扩展。这不仅允许我们询问变量之间的线性关系,还允许我们询问任意类型的两个随机变量之间共享的最大信息。

现在,让我们从头开始实现互信息。

def mutual_information(p_xy, p_x, p_y):
    p = p_xy / (p_x * p_y)
    mutual = p_xy * torch.log2(p)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(mutual)
    return out

mutual_information(torch.tensor([[0.1, 0.5], [0.1, 0.3]]),
                   torch.tensor([0.2, 0.8]), torch.tensor([[0.75, 0.25]]))
tensor(0.7195)
def mutual_information(p_xy, p_x, p_y):
    p = p_xy / (p_x * p_y)
    mutual = p_xy * np.log2(p)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(mutual.as_nd_ndarray())
    return out

mutual_information(np.array([[0.1, 0.5], [0.1, 0.3]]),
                   np.array([0.2, 0.8]), np.array([[0.75, 0.25]]))
[0.7194603]
<NDArray 1 @cpu(0)>
def mutual_information(p_xy, p_x, p_y):
    p = p_xy / (p_x * p_y)
    mutual = p_xy * log2(p)
    # Operator `nansum` will sum up the non-nan number
    out = nansum(mutual)
    return out

mutual_information(tf.constant([[0.1, 0.5], [0.1, 0.3]]),
                   tf.constant([0.2, 0.8]), tf.constant([[0.75, 0.25]]))
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([0.60246783, 0.1169925 ], dtype=float32)>

22.11.3.4. 互信息的性质

与其记住互信息的定义 (22.11.18),你只需要记住它的显著性质即可:

  • 互信息是对称的,即 \(I(X, Y) = I(Y, X)\)

  • 互信息是非负的,即 \(I(X, Y) \geq 0\)

  • \(I(X, Y) = 0\) 当且仅当 \(X\)\(Y\) 是独立的。例如,如果 \(X\)\(Y\) 是独立的,那么知道 \(Y\) 不会提供任何关于 \(X\) 的信息,反之亦然,所以它们的互信息为零。

  • 另外,如果 \(X\)\(Y\) 的一个可逆函数,那么 \(Y\)\(X\) 共享所有信息,并且

    (22.11.19)\[I(X, Y) = H(Y) = H(X).\]

22.11.3.5. 逐点互信息

当我们在本章开始时处理熵时,我们能够将 \(-\log(p_X(x))\) 解释为我们对特定结果的*意外*程度。我们可以对互信息中的对数项给出类似的解释,这个对数项通常被称为*逐点互信息*(pointwise mutual information):

(22.11.20)\[\textrm{pmi}(x, y) = \log\frac{p_{X, Y}(x, y)}{p_X(x) p_Y(y)}.\]

我们可以将 (22.11.20) 视为衡量特定结果组合 \(x\)\(y\) 的可能性与我们对独立随机结果的预期相比有多大或多小。如果它很大且为正,那么这两个特定结果出现的频率远高于随机情况下(*注意*:分母是 \(p_X(x) p_Y(y)\),即两个结果独立时的概率);而如果它很大且为负,则表示这两个结果发生的频率远低于我们通过随机机会预期的频率。

这使我们能够将互信息 (22.11.18) 解释为,与我们期望它们独立发生的情况相比,我们对看到两个结果一起发生感到意外的平均程度。

22.11.3.6. 互信息的应用

互信息在其纯粹的定义中可能有些抽象,那么它与机器学习有何关系呢?在自然语言处理中,最困难的问题之一是*歧义消解*,即词语的含义在上下文中不明确的问题。例如,最近新闻头条报道说“亚马逊着火了”。你可能会想,是亚马逊公司的某栋建筑着火了,还是亚马逊雨林着火了。

在这种情况下,互信息可以帮助我们解决这种歧义。我们首先找到一组与“亚马逊”公司具有较大互信息的词语,如电子商务、技术和在线。其次,我们找到另一组与“亚马逊”雨林具有较大互信息的词语,如雨、森林和热带。当我们需要消除“亚马逊”的歧义时,我们可以比较哪组词在“亚马逊”一词的上下文中出现得更多。在这种情况下,文章会继续描述森林,从而使上下文变得清晰。

22.11.4. KL散度(Kullback–Leibler Divergence)

正如我们在 第 2.3 节 中讨论的,我们可以使用范数来衡量任意维度空间中两点之间的距离。我们希望能够对概率分布做类似的任务。有很多方法可以做到这一点,但信息论提供了最好的方法之一。我们现在探讨*KL散度*(Kullback-Leibler, KL),它提供了一种衡量两个分布是否接近的方法。

22.11.4.1. 定义

给定一个服从概率分布 \(P\) 的随机变量 \(X\),其概率密度函数(p.d.f.)或概率质量函数(p.m.f.)为 \(p(x)\),我们用另一个概率分布 \(Q\)(其 p.d.f. 或 p.m.f. 为 \(q(x)\))来估计 \(P\)。那么,\(P\)\(Q\) 之间的*KL散度*(或称*相对熵*)为:

(22.11.21)\[D_{\textrm{KL}}(P\|Q) = E_{x \sim P} \left[ \log \frac{p(x)}{q(x)} \right].\]

与逐点互信息 (22.11.20) 类似,我们同样可以对对数项给出解释:\(-\log \frac{q(x)}{p(x)} = -\log(q(x)) - (-\log(p(x)))\),如果我们在 \(P\) 下看到 \(x\) 的频率远高于我们在 \(Q\) 下的预期,这个值将是大的正数;如果看到的频率远低于预期,这个值将是大的负数。这样,我们可以将其解释为我们观察到该结果时的*相对*意外程度,与我们从参考分布中观察到它时的意外程度相比。

让我们从头开始实现KL散度。

def kl_divergence(p, q):
    kl = p * torch.log2(p / q)
    out = nansum(kl)
    return out.abs().item()
def kl_divergence(p, q):
    kl = p * np.log2(p / q)
    out = nansum(kl.as_nd_ndarray())
    return out.abs().asscalar()
def kl_divergence(p, q):
    kl = p * log2(p / q)
    out = nansum(kl)
    return tf.abs(out).numpy()

22.11.4.2. KL散度的性质

让我们来看一下KL散度 (22.11.21) 的一些性质。

  • KL散度是非对称的,即存在 \(P,Q\) 使得

    (22.11.22)\[D_{\textrm{KL}}(P\|Q) \neq D_{\textrm{KL}}(Q\|P).\]
  • KL散度是非负的,即:

    (22.11.23)\[D_{\textrm{KL}}(P\|Q) \geq 0.\]

    注意,等号仅在 \(P = Q\) 时成立。

  • 如果存在一个 \(x\) 使得 \(p(x) > 0\)\(q(x) = 0\),那么 \(D_{\textrm{KL}}(P\|Q) = \infty\)

  • KL散度与互信息之间有密切关系。除了在图 22.11.1中显示的关系外,\(I(X, Y)\) 在数值上也等价于以下各项:

    1. \(D_{\textrm{KL}}(P(X, Y) \ \| \ P(X)P(Y))\);

    2. \(E_Y \{ D_{\textrm{KL}}(P(X \mid Y) \ \| \ P(X)) \}\);

    3. \(E_X \{ D_{\textrm{KL}}(P(Y \mid X) \ \| \ P(Y)) \}\).

    对于第一项,我们将互信息解释为 \(P(X, Y)\)\(P(X)\)\(P(Y)\) 的乘积之间的KL散度,因此它衡量了联合分布与独立情况下的分布有多大不同。对于第二项,互信息告诉我们,通过学习 \(X\) 的分布值,关于 \(Y\) 的不确定性的平均减少量。第三项类似。

22.11.4.3. 示例

让我们通过一个简单的例子来明确地看看非对称性。

首先,让我们生成并排序三个长度为 \(10,000\) 的张量:一个目标张量 \(p\),它服从正态分布 \(N(0, 1)\);以及两个候选张量 \(q_1\)\(q_2\),它们分别服从正态分布 \(N(-1, 1)\)\(N(1, 1)\)

torch.manual_seed(1)

tensor_len = 10000
p = torch.normal(0, 1, (tensor_len, ))
q1 = torch.normal(-1, 1, (tensor_len, ))
q2 = torch.normal(1, 1, (tensor_len, ))

p = torch.sort(p)[0]
q1 = torch.sort(q1)[0]
q2 = torch.sort(q2)[0]
random.seed(1)

nd_len = 10000
p = np.random.normal(loc=0, scale=1, size=(nd_len, ))
q1 = np.random.normal(loc=-1, scale=1, size=(nd_len, ))
q2 = np.random.normal(loc=1, scale=1, size=(nd_len, ))

p = np.array(sorted(p.asnumpy()))
q1 = np.array(sorted(q1.asnumpy()))
q2 = np.array(sorted(q2.asnumpy()))
tensor_len = 10000
p = tf.random.normal((tensor_len, ), 0, 1)
q1 = tf.random.normal((tensor_len, ), -1, 1)
q2 = tf.random.normal((tensor_len, ), 1, 1)

p = tf.sort(p)
q1 = tf.sort(q1)
q2 = tf.sort(q2)

由于 \(q_1\)\(q_2\) 关于y轴(即 \(x=0\))对称,我们期望 \(D_{\textrm{KL}}(p\|q_1)\)\(D_{\textrm{KL}}(p\|q_2)\) 之间的KL散度值相似。如下所示,\(D_{\textrm{KL}}(p\|q_1)\)\(D_{\textrm{KL}}(p\|q_2)\) 之间的差异不到3%。

kl_pq1 = kl_divergence(p, q1)
kl_pq2 = kl_divergence(p, q2)
similar_percentage = abs(kl_pq1 - kl_pq2) / ((kl_pq1 + kl_pq2) / 2) * 100

kl_pq1, kl_pq2, similar_percentage
(8582.0341796875, 8828.3095703125, 2.8290698237936858)
kl_pq1 = kl_divergence(p, q1)
kl_pq2 = kl_divergence(p, q2)
similar_percentage = abs(kl_pq1 - kl_pq2) / ((kl_pq1 + kl_pq2) / 2) * 100

kl_pq1, kl_pq2, similar_percentage
(8470.638, 8664.998, 2.268492904612395)
kl_pq1 = kl_divergence(p, q1)
kl_pq2 = kl_divergence(p, q2)
similar_percentage = abs(kl_pq1 - kl_pq2) / ((kl_pq1 + kl_pq2) / 2) * 100

kl_pq1, kl_pq2, similar_percentage
(8652.75, 8690.211, 0.43200163611047165)

相比之下,你可能会发现 \(D_{\textrm{KL}}(q_2 \|p)\)\(D_{\textrm{KL}}(p \| q_2)\) 的差异很大,如下所示,相差约40%。

kl_q2p = kl_divergence(q2, p)
differ_percentage = abs(kl_q2p - kl_pq2) / ((kl_q2p + kl_pq2) / 2) * 100

kl_q2p, differ_percentage
(14130.125, 46.18621024399691)
kl_q2p = kl_divergence(q2, p)
differ_percentage = abs(kl_q2p - kl_pq2) / ((kl_q2p + kl_pq2) / 2) * 100

kl_q2p, differ_percentage
(13536.835, 43.88680093791528)
kl_q2p = kl_divergence(q2, p)
differ_percentage = abs(kl_q2p - kl_pq2) / ((kl_q2p + kl_pq2) / 2) * 100

kl_q2p, differ_percentage
(13416.902, 42.761724211717066)

22.11.5. 交叉熵

如果你对信息论在深度学习中的应用感到好奇,这里有一个简单的例子。我们定义真实分布 \(P\),其概率分布为 \(p(x)\),以及估计分布 \(Q\),其概率分布为 \(q(x)\),我们将在本节的其余部分使用它们。

假设我们需要根据给定的 \(n\) 个数据样本 {\(x_1, \ldots, x_n\)} 解决一个二分类问题。假设我们分别用 \(1\)\(0\) 编码正类和负类标签 \(y_i\),并且我们的神经网络由 \(\theta\) 参数化。如果我们旨在找到一个最佳的 \(\theta\) 使得 \(\hat{y}_i= p_{\theta}(y_i \mid x_i)\),那么应用最大对数似然法是很自然的,正如在 第 22.7 节 中所见。具体来说,对于真实标签 \(y_i\) 和预测 \(\hat{y}_i= p_{\theta}(y_i \mid x_i)\),被分类为正类的概率是 \(\pi_i= p_{\theta}(y_i = 1 \mid x_i)\)。因此,对数似然函数将是

(22.11.24)\[\begin{split}\begin{aligned} l(\theta) &= \log L(\theta) \\ &= \log \prod_{i=1}^n \pi_i^{y_i} (1 - \pi_i)^{1 - y_i} \\ &= \sum_{i=1}^n y_i \log(\pi_i) + (1 - y_i) \log (1 - \pi_i). \\ \end{aligned}\end{split}\]

最大化对数似然函数 \(l(\theta)\) 等同于最小化 \(- l(\theta)\),因此我们可以从这里找到最佳的 \(\theta\)。为了将上述损失推广到任何分布,我们也称 \(-l(\theta)\) 为*交叉熵损失* \(\textrm{CE}(y, \hat{y})\),其中 \(y\) 服从真实分布 \(P\)\(\hat{y}\) 服从估计分布 \(Q\)

这一切都是从最大似然的角度推导出来的。然而,如果我们仔细观察,会发现像 \(\log(\pi_i)\) 这样的项已经进入了我们的计算中,这有力地表明我们可以从信息论的角度来理解这个表达式。

22.11.5.1. 正式定义

与KL散度类似,对于一个随机变量 \(X\),我们也可以通过*交叉熵*来衡量估计分布 \(Q\) 和真实分布 \(P\) 之间的差异:

(22.11.25)\[\textrm{CE}(P, Q) = - E_{x \sim P} [\log(q(x))].\]

利用上面讨论的熵的性质,我们也可以将其解释为熵 \(H(P)\)\(P\)\(Q\) 之间KL散度的和,即:

(22.11.26)\[\textrm{CE} (P, Q) = H(P) + D_{\textrm{KL}}(P\|Q).\]

我们可以如下实现交叉熵损失。

def cross_entropy(y_hat, y):
    ce = -torch.log(y_hat[range(len(y_hat)), y])
    return ce.mean()
def cross_entropy(y_hat, y):
    ce = -np.log(y_hat[range(len(y_hat)), y])
    return ce.mean()
def cross_entropy(y_hat, y):
    # `tf.gather_nd` is used to select specific indices of a tensor.
    ce = -tf.math.log(tf.gather_nd(y_hat, indices = [[i, j] for i, j in zip(
        range(len(y_hat)), y)]))
    return tf.reduce_mean(ce).numpy()

现在定义两个张量用于标签和预测,并计算它们的交叉熵损失。

labels = torch.tensor([0, 2])
preds = torch.tensor([[0.3, 0.6, 0.1], [0.2, 0.3, 0.5]])

cross_entropy(preds, labels)
tensor(0.9486)
labels = np.array([0, 2])
preds = np.array([[0.3, 0.6, 0.1], [0.2, 0.3, 0.5]])

cross_entropy(preds, labels)
array(0.94856)
labels = tf.constant([0, 2])
preds = tf.constant([[0.3, 0.6, 0.1], [0.2, 0.3, 0.5]])

cross_entropy(preds, labels)
0.94856

22.11.5.2. 性质

正如本节开头所提到的,交叉熵 (22.11.25) 可用于定义优化问题中的损失函数。事实证明,以下三者是等价的:

  1. 最大化分布 \(P\)\(Q\) 的预测概率,(即, \(E_{x \sim P} [\log (q(x))]\));

  2. 最小化交叉熵 \(\textrm{CE} (P, Q)\)

  3. 最小化KL散度 \(D_{\textrm{KL}}(P\|Q)\)

交叉熵的定义间接证明了目标2和目标3之间的等价关系,只要真实数据的熵 \(H(P)\) 是常数。

22.11.5.3. 交叉熵作为多分类的目标函数

如果我们深入研究使用交叉熵损失 \(\textrm{CE}\) 的分类目标函数,我们会发现最小化 \(\textrm{CE}\) 等价于最大化对数似然函数 \(L\)

首先,假设我们有一个包含 \(n\) 个样本的数据集,它可以被分为 \(k\) 个类别。对于每个数据样本 \(i\),我们用*独热编码*(one-hot encoding)表示任意一个 \(k\) 类标签 \(\mathbf{y}_i = (y_{i1}, \ldots, y_{ik})\)。具体来说,如果样本 \(i\) 属于类别 \(j\),那么我们将第 \(j\) 个元素设为 \(1\),其他所有分量设为 \(0\),即:

(22.11.27)\[\begin{split}y_{ij} = \begin{cases}1 & j \in J; \\ 0 &\textrm{其他.}\end{cases}\end{split}\]

例如,如果一个多分类问题包含三个类别 \(A\)\(B\)\(C\),那么标签 \(\mathbf{y}_i\) 可以被编码为 {\(A: (1, 0, 0); B: (0, 1, 0); C: (0, 0, 1)\)}。

假设我们的神经网络由 \(\theta\) 参数化。对于真实标签向量 \(\mathbf{y}_i\) 和预测

(22.11.28)\[\hat{\mathbf{y}}_i= p_{\theta}(\mathbf{y}_i \mid \mathbf{x}_i) = \sum_{j=1}^k y_{ij} p_{\theta} (y_{ij} \mid \mathbf{x}_i).\]

因此,*交叉熵损失*将是

(22.11.29)\[\begin{split}\textrm{CE}(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{i=1}^n \mathbf{y}_i \log \hat{\mathbf{y}}_i = - \sum_{i=1}^n \sum_{j=1}^k y_{ij} \log{p_{\theta} (y_{ij} \mid \mathbf{x}_i)}.\\\end{split}\]

另一方面,我们也可以通过最大似然估计来处理这个问题。首先,让我们快速介绍一个\(k\)类多项分布(multinoulli distribution)。它是伯努利分布从二元类到多类的扩展。如果一个随机变量 \(\mathbf{z} = (z_{1}, \ldots, z_{k})\) 服从一个具有概率 \(\mathbf{p} =\) (\(p_{1}, \ldots, p_{k}\)) 的\(k\)类*多项分布*,即

(22.11.30)\[p(\mathbf{z}) = p(z_1, \ldots, z_k) = \textrm{Multi} (p_1, \ldots, p_k), \textrm{ 其中 } \sum_{i=1}^k p_i = 1,\]

那么 \(\mathbf{z}\) 的联合概率质量函数(p.m.f.)是

(22.11.31)\[\mathbf{p}^\mathbf{z} = \prod_{j=1}^k p_{j}^{z_{j}}.\]

可以看出,每个数据样本的标签 \(\mathbf{y}_i\) 服从一个具有概率 \(\boldsymbol{\pi} =\) (\(\pi_{1}, \ldots, \pi_{k}\)) 的 \(k\) 类多项分布。因此,每个数据样本 \(\mathbf{y}_i\) 的联合概率质量函数是 \(\mathbf{\pi}^{\mathbf{y}_i} = \prod_{j=1}^k \pi_{j}^{y_{ij}}.\) 因此,对数似然函数将是

(22.11.32)\[\begin{split}\begin{aligned} l(\theta) = \log L(\theta) = \log \prod_{i=1}^n \boldsymbol{\pi}^{\mathbf{y}_i} = \log \prod_{i=1}^n \prod_{j=1}^k \pi_{j}^{y_{ij}} = \sum_{i=1}^n \sum_{j=1}^k y_{ij} \log{\pi_{j}}.\\ \end{aligned}\end{split}\]

由于在最大似然估计中,我们通过让 \(\pi_{j} = p_{\theta} (y_{ij} \mid \mathbf{x}_i)\) 来最大化目标函数 \(l(\theta)\)。因此,对于任何多分类问题,最大化上述对数似然函数 \(l(\theta)\) 等价于最小化CE损失 \(\textrm{CE}(y, \hat{y})\)

为了检验上述证明,让我们应用内置的度量 NegativeLogLikelihood。使用与前面示例中相同的 labelspreds,我们将得到与前一个示例相同的数值损失,精确到小数点后5位。

# Implementation of cross-entropy loss in PyTorch combines `nn.LogSoftmax()`
# and `nn.NLLLoss()`
nll_loss = NLLLoss()
loss = nll_loss(torch.log(preds), labels)
loss
tensor(0.9486)
nll_loss = NegativeLogLikelihood()
nll_loss.update(labels.as_nd_ndarray(), preds.as_nd_ndarray())
nll_loss.get()
('nll-loss', 0.9485599994659424)
def nll_loss(y_hat, y):
    # Convert labels to one-hot vectors.
    y = tf.keras.utils.to_categorical(y, num_classes= y_hat.shape[1])
    # We will not calculate negative log-likelihood from the definition.
    # Rather, we will follow a circular argument. Because NLL is same as
    # `cross_entropy`, if we calculate cross_entropy that would give us NLL
    cross_entropy = tf.keras.losses.CategoricalCrossentropy(
        from_logits = True, reduction = tf.keras.losses.Reduction.NONE)
    return tf.reduce_mean(cross_entropy(y, y_hat)).numpy()

loss = nll_loss(tf.math.log(preds), labels)
loss
0.94856

22.11.6. 总结

  • 信息论是一个关于信息的编码、解码、传输和处理的研究领域。

  • 熵是衡量不同信号中包含多少信息的单位。

  • KL散度也可以用来衡量两个分布之间的差异。

  • 交叉熵可以被视为多分类的目标函数。最小化交叉熵损失等同于最大化对数似然函数。

22.11.7. 练习

  1. 验证第一节中的扑克牌示例确实具有所声称的熵值。

  2. 证明对于所有分布 \(p\)\(q\),KL散度 \(D(p\|q)\) 是非负的。提示:使用詹森不等式,即利用 \(-\log x\) 是凸函数的事实。

  3. 让我们从几个数据源计算熵

    • 假设你正在观察一只猴子在打字机上产生的输出。这只猴子随机按下打字机的 \(44\) 个键中的任何一个(你可以假设它还没有发现任何特殊键或shift键)。你观察到的每个字符的随机性有多少比特?

    • 对猴子的表现不满意,你用一个喝醉的排字工代替了它。它能生成单词,尽管不连贯。相反,它从一个包含 \(2,000\) 个单词的词汇表中随机选择一个单词。让我们假设英语中一个单词的平均长度是 \(4.5\) 个字母。现在你观察到的每个字符的随机性有多少比特?

    • 仍然对结果不满意,你用一个高质量的语言模型替换了排字工。该语言模型目前可以达到每个单词低至 \(15\) 点的困惑度。语言模型的字符*困惑度*定义为一组概率的几何平均数的倒数,每个概率对应于单词中的一个字符。具体来说,如果给定单词的长度为 \(l\),则 \(\textrm{PPL}(\textrm{word}) = \left[\prod_i p(\textrm{character}_i)\right]^{ -\frac{1}{l}} = \exp \left[ - \frac{1}{l} \sum_i{\log p(\textrm{character}_i)} \right].\) 假设测试单词有4.5个字母,你现在观察到的每个字符的随机性有多少比特?

  4. 直观地解释为什么 \(I(X, Y) = H(X) - H(X \mid Y)\)。然后,通过将两边表示为关于联合分布的期望来证明这是正确的。

  5. 两个高斯分布 \(\mathcal{N}(\mu_1, \sigma_1^2)\)\(\mathcal{N}(\mu_2, \sigma_2^2)\) 之间的KL散度是什么?