基于统计与概率的学习模型
本章讨论概率论和统计学中与机器学习模型相关的一些方面,以及如何使用 Python 对其进行计算。本教材中的全部代码,尤其是本章代码,可以在 https://sohoware.ru/SohoBook/ 找到。 构建机器学习模型通常是为了基于可用数据以及对这些数据的知识进行预测、分类或识别。预测可以是确定性的,也可以是概率性的。很多时候,我们希望预测某个事件发生的概率,这对于解决某些问题可能非常有用并且具有实践价值。 例如,从事飞机维护的工程师可能希望基于记录和/或诊断数据来估计发动机发生故障的概率。对医生而言,基于患者病历、诊断数据和当前流行病学状况,预测患者在近期发展为危重疾病的概率可能很重要。医疗机构则关注预测大流行发生的概率。所有这些任务都需要对事件发生概率进行定量评估的方法。这可能是一个复杂的研究领域,...
关键思想
- 4.1 事件概率的估计与分析
- 4.2 概率论基础
- 4.3 随机数分布
- 4.4 熵与概率论
- 4.5 Kullback–Leibler 散度
- 4.6 机器学习中二元交叉熵的基础
实践任务
选取一个与“基于统计与概率的学习模型”相关的小例子,说明机器学习、特征在其中如何发挥作用,并用一句话解释结果。
基于统计与概率的学习模型
本章讨论概率论和统计学中与机器学习模型相关的一些方面,以及如何使用 Python 对其进行计算。本教材中的全部代码,尤其是本章代码,可以在 https://sohoware.ru/SohoBook/ 找到。
构建机器学习模型通常是为了基于可用数据以及对这些数据的知识进行预测、分类或识别。预测可以是确定性的,也可以是概率性的。很多时候,我们希望预测某个事件发生的概率,这对于解决某些问题可能非常有用并且具有实践价值。
例如,从事飞机维护的工程师可能希望基于记录和/或诊断数据来估计发动机发生故障的概率。对医生而言,基于患者病历、诊断数据和当前流行病学状况,预测患者在近期发展为危重疾病的概率可能很重要。医疗机构则关注预测大流行发生的概率。所有这些任务都需要对事件发生概率进行定量评估的方法。这可能是一个复杂的研究领域,而机器学习模型可以在其中提供帮助。
本章重点介绍构建基于概率和统计的机器学习模型可能需要的基本概念、理论、公式和计算方法。在本章末尾,将给出一个基于朴素贝叶斯分类器的分类模型。
4.1 事件概率的估计与分析
4.1.1 受控随机抽样:方法与策略
在机器学习中,经常需要随机选择数字。这在模拟随机过程、生成测试数据,或者把样本划分为训练组和测试组时尤其常见。为了完成这些任务,可以使用 Python 语言的功能,特别是 random 模块;该模块提供了大量用于生成随机数和随机样本的函数。
Python 中用于生成随机整数的基本工具之一是 random.randint(a, b) 函数。它返回一个随机整数 N,满足 a ≤ N ≤ b,并在从 a 到 b 的给定范围内提供均匀的概率分布。
下面给出用该函数生成随机整数的示例:
import random # Импортируем модуль для работы со случайными числами
# Задаем параметры для генерации чисел
na, nb, n = 1, 100, 5 # 'na' и 'nb' определяют диапазон, 'n' - количество чисел
# Генерируем и выводим n случайных чисел в диапазоне от 'na' до 'nb'
for i in range(n):
print(random.randint(na, nb), ' ', end='') # Генерация и печать случайного числа
print('\n') # Переход на новую строку после вывода всех чисел
# Повторяем процесс для демонстрации независимости генераций
for i in range(n):
print(random.randint(na, nb), ' ', end='')59 69 26 31 23
70 44 43 77 85在上面的代码中,我们生成了两组各包含五个随机整数的序列。每次生成都会得到不同的数字,这说明这些数字具有随机性,并且彼此独立。
下面再来看 random.seed() 函数的使用。该函数允许初始化内部随机数生成器,从而获得可复现的随机数序列。当需要保证实验或分析可以重复时,这一点尤其有用。
import random
# Задаем параметры для генерации чисел
na, nb, n = 1, 100, 5 # 'na' и 'nb' определяют диапазон, 'n' - количество чисел
random.seed(1) # Инициализируем генератор случайных чисел с начальным значением 1
# Генерируем и выводим n случайных чисел с фиксированным начальным значением
for i in range(n):
print(random.randint(na, nb), ' ', end='')
print('\n') # Переход на новую строку
# Повторяем процесс с тем же начальным значением для демонстрации воспроизводимости
random.seed(1) # Используем то же начальное значение
for i in range(n):
print(random.randint(na, nb), ' ', end='')18 73 98 9 33
18 73 98 9 33设置初始值(seed value)可以使每次运行代码时得到相同的结果,这对于科学研究中的实验复现以及软件开发中的测试非常重要。
random.seed() 函数会初始化随机数生成器的内部状态,使之后生成的数字序列变得可预测。在调试程序时,或者在需要复现特定结果以便演示或测试的场景中,这一点非常必要。
虽然标准随机数生成函数生成的数字看起来是随机的,但它们实际上只是伪随机数,因为它们来自确定性的计算过程。在经典计算理论中,计算机按照严格规定的指令运行,因此很难获得真正意义上的随机性。通常使用的是伪随机性,也就是说,数字由算法生成;如果知道生成器的初始状态,就可以预测它们。
下面来看一个在 0 到 1 范围内生成伪随机实数的具体例子。使用 random.random() 函数时,序列中的每个数字都基于先前状态生成,从而形成一个看起来随机的数字序列。
代码如下:
import random # Импорт модуля для работы со случайными числами
# Установка начального значения для генератора случайных чисел обеспечивает воспроизводимость результатов
random.seed(1) # Можно изменить на любое другое число для получения другой последовательности
n = 5 # Количество генерируемых чисел
for i in range(n): # Цикл для генерации n случайных вещественных чисел
print(random.random()) # Выводит случайное вещественное число от 0 до 10.13436424411240122
0.8474337369372327
0.763774618976614
0.2550690257394217
0.49543508709194095这段代码会生成范围在 0 到 1 之间的伪随机实数。为了研究初始值对生成序列的影响,可以改变传递给 random.seed() 的数值,或者暂时注释掉这一行。若不显式设置初始值,通常会使用系统当前时间作为初始值,因此每次执行代码都可能生成不同的序列。
4.2 概率论基础
概率是一种数值度量,用来表示某个事件发生的可能性,或者表示某项预测的可信程度。例如,假设某个结构发生失效的概率为 0.1。这个事实可以用数学方式表示。
在这一语境中,我们只处理一个随机变量,它可以取两个可能的离散值:“是”,其概率为 0.1;以及“否”,其概率为 0.9。这种随机变量分布称为伯努利分布。在更一般的情况下,当研究不同事件时,可能会出现更多离散随机变量,也可能出现具有连续分布的变量。
统计学研究的是对事件数据进行抽样、解释和分析的方法。机器学习基于与某些事件相关的数据集,因此统计分析可以帮助我们理解这些数据集,并可能基于概率进行预测。
为了使用 Python 对数据集进行统计分析,首先需要导入必要的库,包括 TensorFlow:
import tensorflow as tf # Импорт библиотеки TensorFlow для машинного обучения
import numpy as np # Импорт библиотеки NumPy для работы с массивами
# Пример кода, демонстрирующий расчет вероятности с использованием TensorFlow
probability_of_failure = tf.constant(0.1) # Определение вероятности отказа как константы TensorFlow
probability_of_success = 1 - probability_of_failure # Вычисление вероятности успешной работы
# Непосредственный расчет и вывод вероятностей без необходимости использования сессии
print(f"Вероятность отказа: {probability_of_failure.numpy()}")
print(f"Вероятность успеха: {probability_of_success.numpy()}")Вероятность отказа: 0.10000000149011612
Вероятность успеха: 0.8999999761581421考虑一个简单事件:掷一枚有六个相同面的骰子,每个面分别标有从 1 到 6 的唯一数字。在这种情况下,所研究的随机变量可以取 6 个不同的离散值。假设这些标记不会引入任何偏差,也就是说骰子是公平的,并且标记不会影响掷骰结果。
我们的目标是确定经过一系列掷骰之后,某个指定数字出现在骰子上表面的概率。为此,可以在计算机中进行“数值”实验:虚拟地掷骰很多次,并统计感兴趣的数字出现在上表面的次数。
这些实验使我们能够根据大量掷骰中某个数字出现的频率,估计该数字出现在骰子上表面的概率。这种方法基于大数定律:试验次数越多,事件的相对频率就越接近其理论概率。
下面给出可用于执行此类实验的代码示例:
import tensorflow as tf
# Установка начального значения для воспроизводимости результатов
tf.random.set_seed(123)
# Создаем тензор, представляющий равномерное распределение вероятностей для чисел на верхней грани кубика
pr = tf.fill([6], 1/6) # Все значения равны 1/6, представляя равновероятные исходы
# Выводим вероятностное распределение
print('Вероятностное распределение для каждой грани:', pr.numpy())
# Выбираем одно значение из распределения, что соответствует броску кубика
n_top = tf.random.categorical(tf.math.log([pr]), 1) # Используем tf.random.categorical для выборки
# Выводим результат броска кубика
print('Число на верхней грани =', n_top.numpy()[0][0] + 1) # Добавляем 1, потому что индексы начинаются с 0Вероятностное распределение для каждой грани: [0.16666667 0.16666667 0.16666667 0.16666667 0.16666667 0.16666667]
Число на верхней грани = 1如果每次运行时都得到相同的掷骰结果,例如数字 1,这看起来可能令人意外,但它完全可能发生,尤其是在每次运行代码前都把随机数生成器的初始值设置为同一个值时。相同的初始值会保证每次运行所生成的随机数序列相同,因此可能导致每次都得到同一个“随机”数字。
如果希望每次运行时看到不同结果,可以删除 tf.random.set_seed(123) 这一行,或者在每次运行时使用不同的初始值。这样,随机数生成器就会在每次运行时产生不同的数字序列,从而得到不同的掷骰结果。
在该问题中,我们假设某个具体数字出现在骰子上表面的理论概率或“真实”概率为 1/6,约等于 0.1667。一次掷骰会产生一个包含单个元素的一维数组,在 TensorFlow 中即张量;该元素对应骰子的上表面数字。为了计算概率,需要进行大量掷骰,这样统计结果会更加准确。可通过在 tf.random.categorical() 函数中指定张量大小来实现这一点:
import tensorflow as tf
n_surfaces = 6 # количество возможных значений на гранях кубика
n_tosses = 18 # количество бросков
# Установка seed для воспроизводимости результатов
tf.random.set_seed(1)
# Логарифмы вероятностей для каждой грани кубика
logits = tf.math.log([1.0/n_surfaces] * n_surfaces)
# Получение результатов бросков
toss_results = tf.random.categorical(logits[tf.newaxis, :], n_tosses)
print("Бросок", n_tosses, "раз.")
print("Результаты бросков:", tf.squeeze(toss_results).numpy()) # Удаление лишних измерений и преобразование в numpy массивБросок 18 раз.
Результаты бросков: [1 0 2 4 1 0 0 1 3 0 1 3 0 4 0 1 5 1]在这个例子中,我们进行了 18 次掷骰,从而得到一个包含 18 个元素的张量。使用 tf.random.set_seed(1) 可以保证结果可复现。在受控实验中,例如值 5 在 18 次掷骰中出现了 1 次,那么可以得到概率 Pr(骰子="5") = 1/18。如果 3 出现了 3 次,那么 Pr(骰子="3") = 3/18 = 1/6,依此类推。为了更准确地估计概率,可以增加掷骰次数。
import tensorflow as tf
n_surfaces = 6 # количество возможных значений на гранях кубика
n_tosses = 18 # количество бросков
# Установка seed для воспроизводимости результатов
tf.random.set_seed(1)
# Логарифмы вероятностей для каждой грани кубика
logits = tf.math.log([1.0/n_surfaces] * n_surfaces)
# Получение результатов бросков
toss_results = tf.random.categorical(logits[tf.newaxis, :], n_tosses)
print("Бросок", n_tosses, "раз.")
print("Результаты бросков:", tf.squeeze(toss_results).numpy()) # Удаление лишних измерений и преобразование в numpy массив
n_t = 20 # Новое количество бросков
# Проведение новой серии бросков и вывод результатов
new_toss_results = tf.random.categorical(logits[tf.newaxis, :], n_t)
print("Новые результаты бросков:", tf.squeeze(new_toss_results).numpy())Бросок 18 раз.
Результаты бросков: [1 0 2 4 1 0 0 1 3 0 1 3 0 4 0 1 5 1]
Новые результаты бросков: [0 1 3 2 3 3 2 1 0 1 2 5 2 2 4 5 1 2 5 2]下面将掷骰次数增加到 2000 次,并计算每个面的经验概率:
import tensorflow as tf
import numpy as np
n_surfaces = 6 # Количество возможных значений на гранях кубика
n_tosses = 2000 # Количество бросков
# Установка seed для воспроизводимости результатов
tf.random.set_seed(1)
# Логарифмы вероятностей для каждой грани кубика
logits = tf.math.log([1.0/n_surfaces] * n_surfaces)
# Получение результатов бросков
toss_results = tf.random.categorical(logits[tf.newaxis, :], n_tosses)
toss_results = tf.squeeze(toss_results) # Удаление лишних измерений
# Подсчет количества каждой из цифр
counts = tf.math.bincount(toss_results, minlength=n_surfaces)
# Расчет вероятностей для каждой из граней
probabilities = counts / n_tosses
print('Всего бросков:', n_tosses)
print('Вероятность каждой из 6 граней:', probabilities.numpy())
print('Теоретические (истинные) вероятности:', [1.0/n_surfaces] * n_surfaces)Всего бросков: 2000
Вероятность каждой из 6 граней: [0.1655 0.1495 0.1645 0.166 0.1675 0.187 ]
Теоретические (истинные) вероятности: [0.16666666666666666, 0.16666666666666666, 0.16666666666666666, 0.16666666666666666, 0.16666666666666666, 0.16666666666666666]在以下代码中,我们使用 TensorFlow 的功能来统计一系列掷骰过程中每个面出现的次数,并在每一步相对于当前总掷骰次数进行归一化:
import tensorflow as tf
import numpy as np
np.set_printoptions(suppress=True) # Предотвращаем использование научной нотации для больших чисел
n_surfaces = 6 # Количество граней кубика
n_tosses = 2000 # Количество бросков
# Установка seed для воспроизводимости результатов
tf.random.set_seed(1)
# Логарифмы вероятностей для каждой грани кубика
logits = tf.math.log([1.0/n_surfaces] * n_surfaces)
# Получение результатов бросков
toss_results = tf.random.categorical(logits[tf.newaxis, :], n_tosses)
toss_results = tf.squeeze(toss_results) # Удаление лишних измерений
# Инициализация записей результатов
record = tf.Variable(tf.zeros((n_surfaces, n_tosses), dtype=tf.float32))
# Подсчет и запись результатов для каждого броска
for i in range(n_tosses):
# Подсчет количества каждой грани до текущего броска включительно
counts = tf.math.bincount(toss_results[:i+1], minlength=n_surfaces, maxlength=n_surfaces)
record[:, i].assign(tf.cast(counts, tf.float32))
# Нормализация результатов
x = tf.range(1, n_tosses + 1, dtype=tf.float32)
observations = record / x
# Вывод результатов
print("Результаты после первого броска:\n", observations[:, 0].numpy())
print("Результаты после первых 10 бросков:\n", observations[:, 10].numpy())
print("Результаты после первых 1000 бросков:\n", observations[:, 999].numpy())Результаты после первого броска:
[0. 1. 0. 0. 0. 0.]
Результаты после первых 10 бросков:
[0.36363637 0.36363637 0.09090909 0.09090909 0.09090909 0. ]
Результаты после первых 1000 бросков:
[0.157 0.155 0.163 0.17 0.18 0.175]这个简单实验使我们能够得到六个可能值的均匀分布的观测结果,其中骰子的六个面具有相同的出现概率。在 1000 次掷骰后,计算六个面各自的出现概率,通常会得到大约 0.14 到 0.19 之间的数值。由于过程具有随机性,这些概率在每次新实验中都会略有变化。如果每次实验进行 10 000 次掷骰,那么得到的概率会非常接近理论值 1/6 ≈ 0.1667。读者可以使用给出的 TensorFlow 代码很容易地重复这一实验。
现在使用 TensorFlow 和 matplotlib 库对“数值”实验结果进行可视化:
import tensorflow as tf
from matplotlib import pyplot as plt
import numpy as np
np.set_printoptions(suppress=True) # Предотвращаем использование научной нотации для больших чисел
n_surfaces = 6 # Количество граней кубика
n_tosses = 2000 # Количество бросков
# Установка seed для воспроизводимости результатов
tf.random.set_seed(1)
# Логарифмы вероятностей для каждой грани кубика
logits = tf.math.log([1.0/n_surfaces] * n_surfaces)
# Получение результатов бросков
toss_results = tf.random.categorical(logits[tf.newaxis, :], n_tosses)
toss_results = tf.squeeze(toss_results) # Удаление лишних измерений
# Инициализация записей результатов
record = tf.Variable(tf.zeros((n_surfaces, n_tosses), dtype=tf.float32))
# Подсчет и запись результатов для каждого броска
for i in range(n_tosses):
# Подсчет количества каждой грани до текущего броска включительно
counts = tf.math.bincount(toss_results[:i+1], minlength=n_surfaces, maxlength=n_surfaces)
record[:, i].assign(tf.cast(counts, tf.float32))
# Нормализация результатов
x = tf.range(1, n_tosses + 1, dtype=tf.float32)
observations = record / x
# Вывод результатов
print("Результаты после первого броска:\n", observations[:, 0].numpy())
print("Результаты после первых 10 бросков:\n", observations[:, 10].numpy())
print("Результаты после первых 1000 бросков:\n", observations[:, 999].numpy())
# Предполагаем, что observations - это тензор с результатами эксперимента
# Например, observations = tf.random.uniform([6, 1000], minval=0, maxval=1)
# Отрисовываем графики наблюдаемых вероятностей для каждой из граней кости
for i in range(6):
plt.plot(observations[i].numpy(), label=f"Наблюдаемая вероятность P(грань={i+1})")
# Добавляем горизонтальную линию для теоретической вероятности
plt.axhline(y=0.166667, color='black', linestyle='dashed', label="Теоретическая вероятность 1/6")
# Добавляем легенду и отображаем график
plt.legend()
plt.show()进行的实验越多,所得到的概率越接近理论值 1/6。这正是大数定律的体现:当试验次数增加时,重复实验结果的平均值会收敛到期望值。
前面这个简单的掷骰事件示例,直观展示了在更复杂事件的统计分析和概率计算中所使用的一些基本原则与方法。它说明,理解这些基础概念对于分析和解释更复杂的统计模型与概率模型中的结果非常重要。
在复杂事件的语境中,例如具有不同条件的多次掷骰,或者包含多个变量的事件分析,上述简单示例中展示的原则都可以扩展和调整。这可能包括使用更复杂的统计方法和概率分布,也包括利用计算工具和软件处理并分析大规模数据。
4.3 随机数分布
在机器学习中,经常需要对数字进行随机抽样。根据任务类型的不同,变量数据可能服从不同的分布。数值数据的抽样应当基于给定的或假定的分布类型。在本章开头,我们为此使用了均匀分布。现在我们将更详细地讨论这个问题。
4.3.1 均匀分布
基于均匀分布生成的数字,应当有相同的机会落入指定范围内的任意位置。为了用 random.randint() 函数检查所生成数字的均匀性,可以大量执行该函数,例如执行 100 万次,然后观察这些数字如何分布。为此使用下面的代码:
import numpy as np # Импортируем библиотеку для работы с массивами
import matplotlib.pyplot as plt # Импортируем библиотеку для визуализации данных
import random # Импортируем модуль для генерации случайных чисел
na, nb, n = 0, 99, 100 # Задаем начальное и конечное значения диапазона и количество чисел
counts = np.zeros(n) # Создаем массив для подсчета количества каждого сгенерированного числа
# Создаем фигуру для отображения гистограмм
fig, axes = plt.subplots(2, 3, figsize=(15, 8), sharex=True)
axes = axes.reshape(6) # Преобразуем массив осей для удобства обращения
n_samples = 1000001 # Задаем количество выборок
# Генерируем случайные числа и подсчитываем их
for i in range(1, n_samples):
counts[random.randint(na, nb)] += 1 # Увеличиваем счетчик для сгенерированного числа
# Визуализируем распределение чисел на разных этапах генерации
if i in [10, 100, 1000, 10000, 100000, 1000000]:
axes[int(np.log10(i)) - 1].bar(np.arange(na + 1, nb + 2), counts) # Строим гистограмму
plt.show() # Отображаем гистограммы在这段代码中,使用 random.randint(na, nb) 函数生成从 na 到 nb(包含端点)的随机整数。随后,我们统计每个数字被生成的次数,并在生成过程的不同阶段(生成 10、100、1000、10000、100000 和 1000000 个数字之后)用直方图进行可视化。这使我们能够直观地评估数字分布的均匀程度。
根据大数定律,随着样本规模增加,随机抽样结果的平均值会接近总体的数学期望(或平均值)。在均匀分布的语境中,这意味着生成的数字越多,这些数字的分布就越接近理想的均匀分布;在这种理想分布中,给定范围内的每个数字都有相同的机会被选中。
当样本量较小时,可能会出现明显偏离均匀性的情况,因为随机波动可能使某些数字被不成比例地表示。然而,随着样本量增加,这些随机波动会被平均化,数字分布也会变得越来越均匀。这说明,为了获得可靠的统计结论,使用足够大的样本非常重要。
在机器学习语境中还应注意,模型的质量和可靠性在很大程度上取决于所使用数据的数量和质量。在一些任务中,均匀分布的数据尤其重要:如果需要保证可变变量的所有可能取值都得到同等表示,那么模型才能正确学习,并且不会偏向某些特定的取值范围。
4.3.2 高斯分布
正态分布也称为高斯分布,自然界中的许多现象都服从这一规律。它由变量的如下概率密度函数描述:
其中,μ 和 σ 分别是分布的均值和标准差。正态分布通常记作 N(μ, σ²)。特别地,当 μ = 0 且 σ = 1 时,我们得到标准正态分布,记作 N(0, 1),其概率密度函数会简化为相应的标准形式。
NumPy 库的 random 模块中的 gauss() 函数提供了一种方便的方式,用来生成服从正态分布的数字。
让我们把方程中定义的概率密度函数可视化:
这种类似钟形的曲线,你很可能已经很熟悉。
import numpy as np
import matplotlib.pyplot as plt
from random import gauss
mu, sigma, n = 0., 0.1, 10 # Среднее значение (mu), стандартное отклонение (sigma) и количество генерируемых чисел (n)
# Генерация n случайных чисел по нормальному распределению
for i in range(n):
print(f'{gauss(mu, sigma):.4f} ', end='')
# Определение переменной x
x = np.arange(-0.5, 0.5, 0.001)
# Определение функции Гаусса
def gf(mu, sigma, x):
return 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(-0.5 * ((x - mu) / sigma) ** 2)
# Визуализация функции плотности
plt.figure(figsize=(6, 4))
plt.plot(x, gf(mu, sigma, x), label="Гауссова функция")
plt.title("Функция плотности нормального распределения")
plt.xlabel("Значение")
plt.ylabel("Плотность вероятности")
plt.legend()
plt.show()
图:典型正态分布(高斯分布)。
为了从高斯分布中生成随机样本,并将其与“真实”的高斯分布进行比较,可以使用 np.random.normal() 函数。该函数允许我们按照具有指定均值和标准差参数的正态分布生成样本。生成样本后,可以将所得数据可视化为直方图,并把它与理论正态分布曲线进行比较。
import numpy as np
import matplotlib.pyplot as plt
# Задаем параметры нормального распределения
mu, sigma = 0, 0.1 # среднее значение и стандартное отклонение
# Количество генерируемых случайных выборок
n = 500
# Генерация n случайных выборок из нормального распределения
samples = np.random.normal(mu, sigma, n)
# Гистограмма выборок
count, bins, ignored = plt.hist(samples, 80, density=True, alpha=0.6, color='g', label='Гистограмма выборок')
# Функция Гаусса для "истинного" распределения
def gf(mu, sigma, x):
return 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(-0.5 * ((x - mu) / sigma) ** 2)
# Отрисовка теоретической кривой нормального распределения
plt.plot(bins, gf(mu, sigma, bins), linewidth=2, color='r', label='Теоретическое распределение')
plt.title("Сравнение гистограммы выборок с теоретическим нормальным распределением")
plt.xlabel("Значение")
plt.ylabel("Плотность вероятности")
plt.legend()
plt.show()这个示例展示了所生成样本相对于理论正态分布的分布情况,并说明二者之间存在近似一致性。
NumPy 提供了从大约 40 种不同分布类型中生成样本的能力。
4.4 熵与概率论
对于统计事件中随机变量的给定概率,可以计算相应的熵。熵是对某一事件概率分布不确定性的度量。它可以理解为概率向量与其负对数向量之间的标量积;其中概率向量包含随机变量各个可能取值的概率。
设事件的概率分布为 p,则熵可以写作:
H(p) = - Σ p_i log(p_i)其中,p_i 是变量第 i 个可能取值的概率,概率向量 p 包含所有这些概率。负号是必要的,因为对于 0 < p_i < 1,log(p_i) 为负,而熵应为非负量。在计算时,经常还会通过除以可能取值的总数来对其进行归一化。
熵常用于机器学习中的目标函数构造,因为它刻画的是需要被降低或最小化的不确定性。

由于对数函数经常被使用,我们先通过 NumPy 库中的 log() 函数进一步观察它。
import numpy as np
import matplotlib.pyplot as plt
p = np.arange(0.01, 1.0, .01) # 创建从 0.01 到 1、步长为 0.01 的概率数组
logp = -np.log(p) # 计算数组中每个值的负对数
# 绘制负对数图像
plt.plot(p, logp, color='blue')
plt.xlabel('概率 p')
plt.ylabel('p 的负对数')
plt.title('概率负对数的数值')
plt.show()图:概率的对数函数。
下面列出几个重要原因,说明为什么对数在机器学习中如此常用。
函数 −log(p) 随参数 p 单调变化,这对机器学习中广泛使用的优化算法非常重要。单调性很关键,因为对函数取对数不会改变原函数驻点的位置。因此,优化算法的工作更可靠:它保证对损失函数进行最小化或最大化时,会导向一致的最优解。
可以想象你正在上楼梯:每一级台阶都比前一级略高。单调函数的行为与此类似:当你向前迈一步(p 增大)时,函数值只沿一个方向变化(对于 −log(p),它会减小)。这对机器学习问题很有用,因为它有助于“找到”最优解,而不会在方向上产生混乱,就像沿着楼梯走向目标而不是偏离到其他路径一样。
函数 −log(p) 会随着概率 p 的增大而减小,因此它反转了概率变化的趋势,使其适合用来度量熵,尤其是在高概率范围内。当事件发生的概率很高时,不确定性较低,因为我们几乎确信它会发生,所以熵值较小。类似地,当事件发生的概率很低时,不确定性也较低,因为我们几乎确信它不会发生。在熵公式中,这时概率本身也参与了计算。
可以把它想象成一个从 0 到 100 的刻度,其中 100 表示对某件事完全确定,而 0 表示完全不确定。如果某件事的概率为 90 或更高,你几乎确信它会发生,因此你的“不确定性”(或者说如果它发生时的“惊讶程度”)较低。如果某件事的概率非常小,比如 10 或更低,你也几乎确信它不会发生,因此不确定性同样较低。−log(p) 的作用就是:当你对结果越来越确定时,它的值会变小。
熵将概率及其负对数以乘积形式结合起来,如上式所示。这种组合利用了对数函数的性质,并为我们的目的提供了合适的数学行为。
也可以把熵看作对信息“有趣程度”或“意外程度”的度量。如果某件事情非常可能发生(例如白天之后是黑夜),那么它并不十分“有趣”或“意外”,因此它的“有趣程度”(熵)较低。如果某件事不那么可能发生(例如掷普通骰子时出现六点),它就更“有趣”或更“意外”,其熵也更高。通过把概率与它的负对数结合起来,可以构造一个公式,用于衡量不同情形下的“有趣性”或“意外性”;这在机器学习中非常有用,因为它有助于理解我们能多好地预测各种事件的结果。
下面的示例展示熵函数如何工作。
4.4.1 示例 1:概率及其熵
考虑一个变量可以取两个值的事件。我们进行一次观察,得到概率向量 q1,其中包含这两个取值对应的概率;随后又进行一次观察,得到概率向量 q2。我们希望评估这两次观察所对应概率分布的熵。
在这个示例中,可以看到 p 与 −log(p) 的相反行为。向量 q1 对两个变量给出很低或很高的概率值,这意味着不确定性较低,因此计算出的熵较低。另一方面,q2 对两个变量给出接近中间的概率值,这意味着它非常不确定。计算得到的熵较高,这与预期一致。
4.4.2 示例 2:熵的变化
假设一个事件由可以取两个值的变量描述。首先进行一次观察,得到概率向量 q1;然后再进行一次观察,得到概率向量 q2。我们的任务是评估这两个观测结果的概率熵。

为了更好地理解熵的工作原理,可以看下面的程序代码:
import numpy as np
# 用于计算熵的函数
def calculate_entropy(probabilities):
# 对概率向量的每个元素应用负对数,并求和
entropy = -np.sum(probabilities * np.log(probabilities))
return entropy
# 第一次观察的概率向量
q1 = np.array([0.5, 0.5]) # 两种结果的概率示例
# 第二次观察的概率向量
q2 = np.array([0.9, 0.1]) # 显示概率差异的示例
# 计算并输出每个概率向量的熵
entropy_q1 = calculate_entropy(q1)
entropy_q2 = calculate_entropy(q2)
print(f'q1 的熵: {entropy_q1:.4f}')
print(f'q2 的熵: {entropy_q2:.4f}')输出:
q1 的熵: 0.6931
q2 的熵: 0.3251
import numpy as np
# 第一次观察的概率向量,具有较低不确定性:
# 因为变量被观察到的概率要么非常高,要么非常低,
# 所以对事件是否发生具有较强信心。
q1 = np.array([0.999, 0.001])
# 第二次观察的概率向量,具有较高不确定性:
# 因为变量被观察到的概率既不高也不低,
# 所以对事件是否发生缺乏信心。
q2 = np.array([0.5, 0.5])
# 计算并输出概率的负对数,展示其对不确定性的影响
print('q1=', q1, ' -log(q1)=', -np.log(q1))
print('q2=', q2, ' -log(q2)=', -np.log(q2))
# 将熵作为系统不确定性的度量进行计算
H_q1 = -np.dot(q1, np.log(q1)) / len(q1) # q1 的熵
H_q2 = -np.dot(q2, np.log(q2)) / len(q2) # q2 的熵
print('H_q1=', H_q1, 'H_q2=', H_q2)输出:
q1= [0.999 0.001] -log(q1)= [1.00050033e-03 6.90775528e+00]
q2= [0.5 0.5] -log(q2)= [0.69314718 0.69314718]
H_q1= 0.003953627556116044 H_q2= 0.34657359027997264在这个示例中,可以看到 p 与 −log(p) 的相反行为。向量 q1 含有非常低或非常高的概率值,这说明不确定性较低,因此计算得到的熵较低。相反,向量 q2 的两个概率都接近中间值,这表示较高的不确定性。计算得到的熵也如预期那样较高。
为了直观展示熵的变化,构造一个人工事件:变量有两个可能取值。令这两个取值的概率为 v1 和 v2,它们反向变化,并且每一对概率之和等于 1。使用下面的代码计算熵如何随概率变化而变化:
import numpy as np
import matplotlib.pyplot as plt
# 变量可以取两个值的事件
v1 = np.arange(0.01, 1.0, .05) # 从 0.01 到 1.0,以 0.05 为步长初始化 v1
gap = (v1[1] - v1[0]) * len(v1) / 3. # 计算直方图柱之间的宽度
v1 /= (v1[0] + v1[-1]) # 对 v1 归一化,使概率和保持为 1
v2 = v1[::-1] # 创建 v2,作为 v1 的镜像
# 输出 v1 和 v2 的数值用于检查
print(v1, np.sum(v1))
print(v2, np.sum(v2))
# 检查每一对 v1 和 v2 的和是否等于 1
print(v1 + v2, np.sum(v1 + v2) / 2)
# 用直方图可视化概率
xtick = range(len(v1)) # x 轴刻度
plt.bar(range(len(v1)), v1, width=gap * 1.2, alpha=.9, color='blue') # v1 的直方图
plt.bar(range(len(v2)), v2, width=gap, alpha=.9, color='red') # v2 的直方图
plt.xlabel('事件 ID,蓝色:v1,红色:v2')
plt.ylabel('概率')
plt.xticks(xtick)
plt.show()
H_qf = np.array([]) # 初始化事件 q1 的熵数组
# 对每一对概率计算熵
for q1 in list(zip(v1, v2)):
H_qf = np.append(H_qf, -(np.dot(q1, np.log(q1))) / 2) # 计算并追加熵
# 可视化熵的变化
plt.plot(v1, H_qf)
plt.xlabel('概率,v1(v2=1-v1)')
plt.ylabel('事件熵')
plt.title('事件熵')
plt.show()输出示例:
[0.01030928 0.06185567 0.11340206 0.16494845 0.21649485 0.26804124
0.31958763 0.37113402 0.42268041 0.4742268 0.5257732 0.57731959
0.62886598 0.68041237 0.73195876 0.78350515 0.83505155 0.88659794
0.93814433 0.98969072] 10.0
[0.98969072 0.93814433 0.88659794 0.83505155 0.78350515 0.73195876
0.68041237 0.62886598 0.57731959 0.5257732 0.4742268 0.42268041
0.37113402 0.31958763 0.26804124 0.21649485 0.16494845 0.11340206
0.06185567 0.01030928] 9.999999999999998
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.] 10.0从图中可以看到,当概率 v1 和 v2 都等于 0.5 时,熵达到最大值,这对应于结果不确定性最高的情况。熵在区间边缘处最小,对应于事件结果几乎确定的情况。
4.4.3 示例 3:变量取值数量不同且服从均匀分布的事件熵
考虑一些事件,其中变量可以有不同数量的可能取值。假设这些事件中变量的概率分布都是均匀分布。我们的目标是弄清楚:概率分布的熵如何随事件变量可能取值数量的变化而变化。
熵是不确定性或概率分布不可预测性的度量。在均匀分布语境中,变量可能取的值越多,不确定性就越高,相应地熵也越高。

import matplotlib.pyplot as plt
import numpy as np
# 初始化变量
N = 0 # 当前变量可能取值的数量
max_v = 100 # 最大可能取值数量
Ni = np.array([]) # 用于保存变量数量的数组
H_qf = np.array([]) # 用于保存熵值的数组
# 为每个 N 生成均匀分布并计算熵
while N < max_v:
N += 1 # 增加变量可能取值的数量
Ni = np.append(Ni, N) # 记录当前变量数量
qf = np.ones(N) / N # 生成均匀分布
H_qf = np.append(H_qf, -np.dot(qf, np.log(qf)) / len(qf)) # 计算并记录熵
# 输出结果
print('概率分布:', qf[0:max_v:10])
print('H_qf=', H_qf[0:max_v:10])
# 可视化结果
plt.plot(Ni, H_qf)
plt.xlabel('变量数量,所有变量概率相同')
plt.ylabel('熵')
plt.title('均匀分布变量的事件')
plt.show()输出:
概率分布: [0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01]
H_qf= [-0. 0.21799048 0.14497726 0.11077378 0.09057493 0.07709462
0.06739137 0.06003774 0.05425246 0.04956988]在这段代码中,我们研究在均匀分布下,当变量可能取值的数量增加时,熵如何变化。我们从一个可能取值开始,逐步把数量增加到 100;在每一步中,都为当前分布计算并记录熵。
从结果可以看出:
当 N = 1 时,熵等于零。这意味着如果只有一个可能取值,就没有不确定性,我们确切知道结果。
当 N = 3 时,熵达到最大值。这表明在这种计算方式下,给定可能取值数量对应的最大不确定性出现在这一点附近。
当 N 非常大时,熵会下降;这是因为在所有变量都服从均匀分布的假设下,每个单独变量的概率都变得非常小,从而导致按这里归一化方式计算的不确定性降低。
这个实验强调了系统可能状态数量与系统不确定性或熵之间的联系,尤其是在均匀分布的语境中。
4.4.4 交叉熵
交叉熵是机器学习和统计学中经常遇到的概念,它帮助我们衡量预测与真实数据匹配得有多好。为了理解这个概念,可以想象有一个程序试图猜测用户在屏幕上画了什么。
假设程序可以预测三种对象:猫、树和汽车。当用户画出一幅图时,程序会分析图像,并给出它分别是猫、树或汽车的概率。这些概率构成了我们的“预测分布”。
“真实分布”是用户实际上画的对象。如果用户画的是猫,那么真实分布就是 100% 猫,其他对象都是 0%。
交叉熵帮助我们理解预测分布与真实分布的匹配程度。如果预测非常接近现实,交叉熵会较低;如果预测偏离真实情况很远,交叉熵会较高。
下面的 Python 示例展示如何计算交叉熵:
import numpy as np
# 真实分布:用户画的是猫
true_distribution = np.array([1, 0, 0]) # 猫、树、汽车
# 预测分布:程序认为这最可能是猫
predicted_distribution = np.array([0.7, 0.2, 0.1])
# 计算交叉熵
cross_entropy = -np.sum(true_distribution * np.log(predicted_distribution))
print(f"交叉熵: {cross_entropy}")输出:
交叉熵: 0.35667494393873245我们把真实分布和预测分布都表示为数组,数组中的每个元素代表所画图像是猫、树或汽车的概率。随后使用公式计算交叉熵,并输出结果。
交叉熵经常用于统计学。分布 q 相对于分布 p 的交叉熵可定义为:
H(p, q) = - Σ p_i log(q_i)一般来说,交叉熵是衡量两个分布(来自同一空间)相似程度的指标。在机器学习中,我们关心的是预测概率 q 相对于真实分布 p 的交叉熵。
在这一语境中,H(p, q) 可作为预测模型效率的度量,因此常被用作机器学习模型中的目标函数或损失函数。
需要注意以下性质:
交叉熵不是对称的:一般来说,如果 p ≠ q,则 H(p, q) ≠ H(q, p)。
通常有 H(p, q) ≥ H(p)。二者之间的差值就是 Kullback–Leibler 散度,它始终非负。当两个分布完全一致时,交叉熵会退化为前一节讨论的熵,上述不等式也变为等式。
因此,在机器学习模型中,即使预测是完美的,交叉熵也不一定等于零,因为真实分布本身可能具有熵。如果 H(p) 是真实分布的熵,那么交叉熵的下界就是 H(p)。只有当真实分布没有任何不确定性时,也就是除一个变量概率为 1 外,其余变量概率都为 0 时,它才可能等于零。
下面来看几个简单示例。
4.4.5 示例 4:质量预测中的交叉熵
在预测语境中的交叉熵部分,我们以天气预报质量评估为例。假设我们希望根据当前气象数据预测第二天降水的概率。作为示例,我们的预测是:下雨概率为 90%,晴天概率为 10%。这就是我们的假设或“预测”。
接着,假设我们拥有第二天天气的准确数据,例如通过时间机器获得。根据这些数据,真实下雨概率为 99%,晴天概率仅为 1%。这些值就是“真实”概率。
为了把我们的预测与真实概率进行比较,可以使用称为交叉熵的方法。交叉熵可以量化模型预测的概率分布与真实概率分布之间的差异。交叉熵的最小值表明预测最接近真实情况,而较大的值说明预测与实际数据之间存在显著差异。
在编程中,这一过程包括计算预测概率分布与实际观测分布之间的交叉熵。它可以评估模型预测真实结果的有效程度,是机器学习和数据分析中的重要工具。


考虑一个简单事件,其中变量有两个可能取值。假设我们有一个质量预测,下面看看如何用交叉熵度量它。
良好预测的情况:
import numpy as np
# 两种结果的预测概率
q_good = np.array([0.9, 0.1]) # 两个取值的预测概率
y = np.array([0.99, 0.01]) # 两个取值的真实概率
p = y # 真实分布
q = q_good # 预测分布
# 输出 p 和 q,以及它们的对数
print('p=', p, ' log(p)=', -np.log(p))
print('q=', q, ' log(q)=', -np.log(q))
# 计算并输出 p 和 q 的熵
print(' 熵: Hp=', -np.dot(p, np.log(p)) / len(p),
' Hq=', -np.dot(q, np.log(q)) / len(q))
# 计算并输出交叉熵
print('\n交叉熵: Hpq=', -np.dot(p, np.log(q)) / len(p),
' Hqp=', -np.dot(q, np.log(p)) / len(q))输出:
p= [0.99 0.01] log(p)= [0.01005034 4.60517019]
q= [0.9 0.1] log(q)= [0.10536052 2.30258509]
熵: Hp= 0.028000767177423672 Hq= 0.1625414866957241
交叉熵: Hpq= 0.06366638071559423 Hqp= 0.2347811604334802np.array([0.9, 0.1]) 和 np.array([0.99, 0.01]) 分别给出了两个可能结果的预测概率和真实概率。
-np.log(p) 和 -np.log(q) 用于计算数组 p 和 q 各元素的自然对数的相反数,这是熵和交叉熵公式中的一部分。
np.dot(p, np.log(p)) 和 np.dot(p, np.log(q)) 计算两个数组之间的标量积,这是计算熵和交叉熵所必需的。除以 len(p)(p 中元素个数)可以对结果进行归一化。

输出熵值 Hp 以及交叉熵 Hpq 和 Hqp,可以帮助我们理解预测质量。较低的交叉熵 Hpq 表明预测分布 q 相对于真实分布 p 的质量较好。
注意,Hpq 和 Hqp 的值不同,这说明交叉熵具有非对称性。同时,一般预期 Hpq ≥ Hp 且 Hqp ≥ Hq,这表明交叉熵大于或等于相应的熵。这个性质可用于评估预测分布相对于真实分布的质量。
可以看到,交叉熵 Hpq 较低,说明预测较好。
4.4.6 示例 5:预测质量较差时的交叉熵
想象一个简单的纸牌游戏,其中只有两张牌:一张红色,一张蓝色。游戏目标是猜测会抽到哪张牌。在实验中,我们先做出预测,然后将其与真实结果比较,以评估预测有多好。
预测:假设我们认为抽到红牌的概率为 10%(0.1),抽到蓝牌的概率为 90%(0.9)。
真实情况:实际上,红牌出现的概率为 99%(0.99),蓝牌只有 1%(0.01)。
为了理解预测与真实情况的差距,我们使用交叉熵这个来自信息论和机器学习的概念。在 Python 中,可以用几行代码实现:
import numpy as np
# 给出预测概率和真实概率
q_bad = np.array([0.1, 0.9]) # 预测
y = np.array([0.99, 0.01]) # 真实情况
# 计算交叉熵
cross_entropy = -np.dot(y, np.log(q_bad))
print(f"交叉熵: {cross_entropy}")输出:
交叉熵: 2.2806128472206835在这个示例中,交叉熵约为 2.28,说明预测与实际概率之间存在显著差异。
交叉熵用于衡量两个概率分布之间的“距离”,常用于分类任务和机器学习模型优化。在纸牌类比中,较大的交叉熵表示如果根据这个预测下注,你会经常出错,因为你的预测与现实差距很大。
交叉熵的数值显示了预测与真实结果之间的差异。数值越高,预测越不准确。在本例中,较高的交叉熵说明“蓝牌以 90% 概率出现”这一假设远离真实情况。
再考虑一次变量有两个可能取值的简单事件。这一次假设预测较差,并分析它如何反映在交叉熵中。
完全错误预测的情况:
import numpy as np
# 两个取值的预测概率(较差预测)
q_bad = np.array([0.1, 0.9])
# 两个取值的真实概率
y = np.array([0.99, 0.01])
p = y # 真实情况
q = q_bad # 预测
# 输出真实概率和预测概率及其对数
print('p=', p, ' log(p)=', -np.log(p))
print('q=', q, ' log(q)=', -np.log(q))
# 计算真实分布和预测分布的熵
print(' 熵: Hp=', -np.dot(p, np.log(p)) / len(p),
' Hq=', -np.dot(q, np.log(q)) / len(p))
# 计算交叉熵
print('\n交叉熵: Hpq=', -np.dot(p, np.log(q)) / len(p),
' Hqp=', -np.dot(q, np.log(p)) / len(q))输出:
p= [0.99 0.01] log(p)= [0.01005034 4.60517019]
q= [0.1 0.9] log(q)= [2.30258509 0.10536052]
熵: Hp= 0.028000767177423672 Hq= 0.1625414866957241
交叉熵: Hpq= 1.1403064236103417 Hqp= 2.072829100487316在这段代码中:
q_bad 表示事件概率的较差预测。
y 包含事件的真实概率。
p 和 q 用于方便表示,分别代表真实概率和预测概率。
np.log() 用于计算概率的自然对数,这是计算熵和交叉熵所必需的。
控制台输出可以显示概率值、它们的对数,以及熵和交叉熵的数值。
从结果可以看出,交叉熵 Hpq 的值较高,这说明预测 q 的质量较差。还应注意,Hpq 不等于 Hqp;一般来说,Hpq ≥ Hp,而 Hqp ≥ Hq,这体现了交叉熵的非对称性,也说明其值取决于哪个分布被视为真实分布、哪个分布被视为预测分布。
可以看到,交叉熵较高,说明预测质量较差。现在我们可以继续讨论 Kullback–Leibler 散度。
4.5 Kullback–Leibler 散度
Kullback–Leibler 散度是一种用于定量评估两个概率分布之间差异的度量。在这里,概率分布可以看作一种数学模型,用来描述某个实验或被观察过程里不同事件或结果发生的概率。
为了便于直观理解,可以想象有两位观察者,他们各自对可观察世界中某些事件或元素的分布有自己的认识。这种认识可以用装有不同颜色玻璃球的“袋子”来表示:每一种颜色对应某个事件或元素,而某种颜色玻璃球的数量反映了该事件在观察者看来发生的概率。
Kullback–Leibler 散度可以用来评估这两种认识相差多大,也就是说,一个“袋子”与另一个“袋子”有多么不同。如果两个“袋子”完全相同,也就是两位观察者对事件分布的理解完全一致,那么 Kullback–Leibler 散度等于零。相反,“袋子”之间的差异越大,KL 散度的值就越大,这表示观察者对事件或元素分布的理解存在更显著的差异。
在更正式的数学语境中,KL 散度可计算为:在一个分布中各事件概率与这些概率相对于另一个分布中对应事件概率之比的对数的乘积之和。这样不仅可以判断两个分布之间是否存在差异,还可以定量评估这种差异的程度。
因此,利用 KL 散度的概念,我们不仅可以比较不同上下文中关于事件或元素分布的不同表示,还可以分析数据中的变化。
在 Python 中,我们可以把概率分布表示为一个概率列表,其中列表的每个元素表示某个事件发生的概率。
假设有两个分布 P 和 Q,它们在 Python 中表示为列表。我们希望使用 KL 散度计算 Q 相对于 P 的差异程度。
import numpy as np# 定义两个分布 P 和 Q
P = np.array([0.1, 0.2, 0.7]) # 分布 P
Q = np.array([0.1, 0.3, 0.6]) # 分布 Q
# 计算从 Q 到 P 的 KL 散度
KL_divergence = np.sum(P * np.log(P / Q))
# 输出结果
print(f"Kullback–Leibler 散度(从 Q 到 P): {KL_divergence}")Kullback–Leibler 散度(从 Q 到 P): 0.026812454257447993Kullback–Leibler 散度(KL 散度)是一个分布相对于另一个分布的相对熵度量。对于两个给定分布 p 和 q,从 p 到 q 的 KL 散度定义为:
D_KL(p || q) = Σ p_i log(p_i / q_i)它也称为 p 相对于 q 的相对熵,其中 q 可以被视为真实分布或参考分布。利用熵和交叉熵的定义,可以写成:
D_KL(p || q) = H(p, q) - H(p)需要注意,p 相对于 q 的 KL 散度不同于 q 相对于 p 的 KL 散度。并且有:
D_KL(p || q) ≥ 0等号仅在两个分布相同的情况下成立:
p = q下面给出两个简单的 KL 散度示例。
4.5.1 示例 1:高质量预测分布的 KL 散度
设想你有两位朋友,他们都非常喜欢冰淇淋,但偏好的口味不同。其中一位通常会选择巧克力味(99% 的情况),只有很少时候会选择草莓味(1% 的情况)。这个偏好分布可以记为 P。
现在,我们尝试根据以往经验预测这位朋友下一次会选择什么口味的冰淇淋。你可以假设:“我认为他有 90% 的概率选择巧克力味,10% 的概率选择草莓味。”这就是你的预测口味分布,我们称之为 Q。
Kullback–Leibler 散度(KL 散度)是一种评估预测分布 Q 与实际分布 P 符合程度的方法。它可以用来衡量你对朋友偏好的了解程度。
如果你的假设几乎与朋友的真实选择一致,那么 KL 散度会很小,这说明你的预测比较准确。然而,如果假设与实际情况相差很大,散度就会较大,这表明需要更好地了解朋友的偏好。
还需要记住,KL 散度并不对称;这意味着重要的不仅是你有多准确地猜中了朋友的偏好,也包括真实偏好与预测之间在不同方向上的差异。它类似于比较朋友按你的建议选择冰淇淋时的感受,以及你按朋友真实偏好选择冰淇淋时的感受。
最后,在代码示例中可以看到,KL 散度的数值较小,这说明预测已经相当准确。
考虑一个简单事件,其变量可以取两个可能值。假设我们有一个相对于真实分布的预测分布。问题是:如何使用 Kullback–Leibler 散度(KL 散度)来衡量预测质量?
在预测较好的情况下,可以定义真实或参考分布 p,以及预测分布 q。关键问题是预测分布与真实分布 p 有多接近,这正是 KL 散度所度量的内容。
KL 散度不是对称的,这意味着 D_KL(p || q) 不等于 D_KL(q || p)。这是一个重要性质,它说明预测分布与真实分布之间的差异在两个方向上并不相同。下面用具体数值来观察这一点。
import numpy as np# 真实或参考分布
p = np.array([0.99, 0.01]) # 两个可能结果的概率
# 预测分布
q_good = np.array([0.9, 0.1]) # 较好的预测分布
# 计算两个分布中每个元素的负对数
log_p = -np.log(p)
log_q_good = -np.log(q_good)
# 输出真实分布和预测分布的数值
print(f'p={p}, log(p)={log_p}')
print(f'q={q_good}, log(q)={log_q_good}')
# 计算从 p 到 q 以及从 q 到 p 的 KL 散度
Dpq = np.sum(p * (log_p - log_q_good)) / len(p)
Dqp = np.sum(q_good * (log_q_good - log_p)) / len(p)
# 输出 KL 散度的数值
print(f'Dpq={Dpq}, Dqp={Dqp}')
# 输出:可以看到两个 KL 散度都很小,这表明预测 q 的质量较好。
p=[0.99 0.01], log(p)=[0.01005034 4.60517019]
q=[0.9 0.1], log(q)=[0.10536052 2.30258509]
Dpq=-0.035665613538170556, Dqp=-0.07223967373775611在这段代码中,我们定义了真实分布 p 和预测分布 q_good,计算了这些分布的对数,然后分别计算两个方向上的 KL 散度。
请注意,两个方向的结果并不相同。
4.5.2 示例 2:预测较差分布的 KL 散度
设想有一枚硬币,它可能正面朝上,也可能反面朝上,而我们试图猜测这两种情况发生的频率。我们做出了一个假设,但它并不准确。下面展示如何使用一种称为 KL 散度的特殊计算方法来检验这一点。
import numpy as np# 我们对硬币不同面朝上的频率所作的假设
# 真实概率:99% 为“正面”,1% 为“反面”
p = np.array([0.99, 0.01])
# 错误预测:10% 为“正面”,90% 为“反面”
q = np.array([0.1, 0.9])
# 计算并显示我们认为硬币各面朝上的频率
print('我们认为“正面”出现于', q[0] * 100, '% 的情况')
print('我们认为“反面”出现于', q[1] * 100, '% 的情况')
# 计算预测中的误差
# 假设“正面”出现频率与真实情况不符时的误差
Dpq = np.sum(p * (np.log(p) - np.log(q)))
# 假设“反面”出现频率与真实情况不符时的误差
Dqp = np.sum(q * (np.log(q) - np.log(p)))
# 显示错误有多大
print('当我们认为“正面”出现得更多时的误差:', Dpq)
print('当我们认为“反面”出现得更多时的误差:', Dqp)我们认为“正面”出现于 10.0 % 的情况
我们认为“反面”出现于 90.0 % 的情况
当我们认为“正面”出现得更多时的误差: 2.224611312865836
当我们认为“反面”出现得更多时的误差: 3.8205752275831846关于硬币“正面”仅在 10% 情况下出现、“反面”在 90% 情况下出现的假设,与真实情况相差很大;真实情况中,“正面”出现于 99% 的情况,“反面”仅出现于 1% 的情况。这意味着我们对概率分布的假设非常不准确。
得到的 KL 散度值(Dpq 和 Dqp)显示了预测中错误的大小。KL 散度越大,分布预测就越差。
KL 散度不是对称的;也就是说,误差值会根据比较方向而改变:是把预测分布与真实分布比较,还是把真实分布与预测分布比较。在这个例子中,当我们假设“反面”比“正面”更常出现时产生的误差(Dqp),大于反向假设中的误差(Dpq)。这表明预测与现实之间的不一致,在不同比较方向上可能产生不同后果。
这个例子强调了在统计学和机器学习中准确预测概率分布的重要性。不准确的预测可能导致显著误差,而这些误差又可能影响基于预测所做出的决策。
再考虑一个与变量有关的简单事件,该变量可以取两个可能值。假设与真实或参考分布相比,我们有一个不准确的概率分布预测。下面的代码示例展示如何用 KL 散度衡量这种差异:
import numpy as np# 较差预测
p = np.array([0.99, 0.01]) # 真实分布
q = np.array([0.1, 0.9]) # 预测分布
# 输出分布及其负对数
print('p=', p, ' log(p)=', -np.log(p))
print('q=', q, ' log(q)=', -np.log(q))
# 计算从 p 到 q 以及从 q 到 p 的 KL 散度
Dpq = np.dot(p, (np.log(p) - np.log(q))) / len(p)
Dqp = np.dot(q, (np.log(q) - np.log(p))) / len(p)
# 输出 KL 散度的数值
print('从 p 到 q 的 KL 散度 (Dpq)=', Dpq)
print('从 q 到 p 的 KL 散度 (Dqp)=', Dqp)
p= [0.99 0.01] log(p)= [0.01005034 4.60517019]
q= [0.1 0.9] log(q)= [2.30258509 0.10536052]从 p 到 q 的 KL 散度 (Dpq)= 1.112305656432918
从 q 到 p 的 KL 散度 (Dqp)= 1.910287613791592可以看到,两个 KL 散度值 Dpq 和 Dqp 都为正。它们都具有较高数值,这说明预测 q 并不准确。还应注意,Dpq ≠ Dqp,这体现了 KL 散度的非对称性。
4.6 机器学习中二元交叉熵的基础
当我们在 Python 中处理分类问题时,尤其是任务需要判断某个对象是否属于两个类别之一(例如“狗”或“猫”)时,常常使用一种称为二元交叉熵的方法来评估算法的有效性。
可以把二元交叉熵理解为衡量程序预测结果与真实答案之间差异的一种方式。如果程序对自己的预测非常确信,但预测错了,那么二元交叉熵会很高,表示误差很大。相反,如果程序的预测准确,二元交叉熵的值会较低,表示程序表现良好。
在 Python 语境中,我们可以使用一个函数来计算二元交叉熵。该函数接收两个主要参数:真实标签(例如,用 1 表示狗,用 0 表示猫)以及每个对象属于类别 1 的预测概率。然后,函数根据预测值与真实标签之间的差异,为每个预测计算“惩罚”,并返回所有预测的总“惩罚”,这就是二元交叉熵。
将二元交叉熵作为评价指标,可以帮助我们改进机器学习算法:通过调整算法,使预测与真实标签之间的差异最小化,从而让模型在分类任务中更加准确。
import numpy as np
def binary_cross_entropy(true_labels, predicted_probs):
"""
计算真实标签与预测概率之间的二元交叉熵。
参数:
true_labels (numpy array): 真实标签数组(0 或 1)。
predicted_probs (numpy array): 属于类别 1 的预测概率数组。
返回:
float: 二元交叉熵的值。
"""
# 为防止对零取对数而加入的最小值
epsilon = 1e-15
# 修正预测概率,避免对数计算错误
predicted_probs = np.clip(predicted_probs, epsilon, 1 - epsilon)
# 计算二元交叉熵
bce = -np.mean(
true_labels * np.log(predicted_probs)
+ (1 - true_labels) * np.log(1 - predicted_probs)
)
return bce
# 使用示例
true_labels = np.array([1, 0, 1, 1, 0]) # 真实标签(1 - 狗,0 - 猫)
predicted_probs = np.array([0.9, 0.2, 0.8, 0.95, 0.1]) # 图像中是狗的预测概率
bce = binary_cross_entropy(true_labels, predicted_probs)
print(f"二元交叉熵: {bce}")输出结果:
二元交叉熵: 0.14166028566632455在这段代码中,函数 binary_cross_entropy 接收两个参数:true_labels,即真实标签数组;以及 predicted_probs,即预测概率数组。函数内部使用 np.clip 操作限制预测概率,避免概率正好等于 0 或 1 时造成对数计算错误。随后计算每个样本的加权对数损失的平均值,这就是该数据集上的最终二元交叉熵。
二元交叉熵是用于二分类任务的两个概率分布之间差异的度量。它在机器学习和统计学中用于衡量真实概率分布与预测概率分布之间的“距离”。在机器学习语境中,真实标签通常为 0 或 1,而预测值表示属于类别 1 的概率。二元交叉熵可以定义为:
对于两个给定分布,二元交叉熵衡量预测分布相对于真实分布的误差。
在机器学习模型中,我们通常假设真实分布可以取 0 或 1,因此不能直接对它进行普通意义上的对数化处理。二元交叉熵可看作预测概率相对于真实标签的熵度量。它既考虑事件发生的概率,也考虑事件不发生的反向概率,并对两者的熵进行计算。本质上,它相当于将交叉熵扩展到正反两类情况,是一种略微增强的、衡量预测分布与真实分布不一致程度的指标。它常用于度量模型性能,也常作为损失函数的一种形式。
下面考虑几个示例。
4.6.1 示例 1:优质预测分布的二元交叉熵
二元交叉熵衡量预测概率分布与真实分布匹配得有多好。在本例中,我们处理的是质量较好的预测,这意味着预测概率分布很好地接近真实分布。例如,如果某事件发生的真实概率为 0.8,而模型预测的概率为 0.85,则两者差异很小,因此二元交叉熵较低,表示预测质量较好。
设想你要预测明天是否会下雨。明天下雨的真实概率为 80%(0.8)。你的天气预测模型给出的下雨概率为 85%(0.85)。由于预测值非常接近真实概率,两者之间的差异很小,因此二元交叉熵较低。这说明你的预测相当不错。
考虑一个简单事件,其变量可以取四个可能值。假设我们得到了一个相对于真实分布或参考分布来说较好的预测分布。下面用代码研究如何通过二元交叉熵来衡量这种情况:
import numpy as np
# 真实概率分布
p = np.array([1.0, 0.0, 0.0, 0.0]) # 真实值
# 预测概率分布
q = np.array([0.9, 0.04, 0.03, 0.03]) # 预测值
# 真实分布和预测分布的反向概率
p_conv = 1.0 - p # 真实值的反面
q_conv = 1.0 - q # 预测值的反面
# 输出真实概率、预测概率及其反向值
print(p, q, ' 反向值:', p_conv, q_conv)
# 计算交叉熵
cHpq = -np.sum(np.dot(p, np.log(q))) / len(p)
# 计算二元交叉熵
bcHpq = -np.sum(np.dot(p, np.log(q)) + np.dot(p_conv, np.log(q_conv))) / len(p)
# 输出交叉熵和二元交叉熵的值
print('交叉熵 cHpq:', cHpq)
print('二元交叉熵 bcHpq:', bcHpq)输出结果:
[1. 0. 0. 0.] [0.9 0.04 0.03 0.03] 反向值: [0. 1. 1. 1.] [0.1 0.96 0.97 0.97]
交叉熵 cHpq: 0.02634012891445657
二元交叉熵 bcHpq: 0.05177523128687465在这个例子中,事件变量可以取四个值之一。假设分布 q 是我们对真实分布 p 的预测。我们使用交叉熵来评估预测相对于真实分布的质量。交叉熵 cHpq 衡量两个分布之间的差异,而二元交叉熵 bcHpq 同时考虑事件发生的概率及其反向概率,因此给出了更完整的差异度量。
可以看到,二元交叉熵大约是普通交叉熵的两倍,这符合预期。原因在于,二元交叉熵不仅考虑事件发生的概率(普通交叉熵所考虑的内容),还考虑事件不发生的概率,因此对二分类器预测质量的评价更加具有信息量。
4.6.2 示例 2:预测较差的分布对应的二元交叉熵
在本例中,我们考虑预测概率分布与真实分布明显不同的情形。例如,如果事件的真实概率为 0.8,而模型预测为 0.3,则预测概率与真实概率之间的差异很大。因此,二元交叉熵的值会较高,表示预测质量较差。
仍以天气预报为例,假设明天下雨的真实概率为 80%(0.8),但你的模型只预测下雨概率为 30%(0.3)。这里,模型预测与真实概率之间差异显著,导致二元交叉熵较高。这说明预测距离真实情况较远,质量不好。
考虑一个事件,其变量可以取四个可能值。假设我们对真实分布得到了一个较差的预测分布。下面再次使用代码观察二元交叉熵如何度量这种情况:
import numpy as np
# 初始化较差的预测和真实分布
q = np.array([0.4, 0.2, 0.3, 0.1]) # 预测
p = np.array([1.0, 0.0, 0.0, 0.0]) # 真实分布
# 计算预测分布和真实分布的反向值
p_conv = 1.0 - p # 真实分布的反面
q_conv = 1.0 - q # 预测分布的反面
# 输出真实分布、预测分布及其反向值
print(f"真实: {p}, 预测: {q}, 真实反向: {p_conv}, 预测反向: {q_conv}")
# 计算交叉熵
cHpq = -np.sum(p * np.log(q)) / len(p)
# 计算二元交叉熵
bcHpq = -np.sum(p * np.log(q) + p_conv * np.log(q_conv)) / len(p)
# 输出结果
print(f"交叉熵 cHpq: {cHpq}")
print(f"二元交叉熵 bcHpq: {bcHpq}")输出结果:
真实: [1. 0. 0. 0.], 预测: [0.4 0.2 0.3 0.1], 真实反向: [0. 1. 1. 1.], 预测反向: [0.6 0.8 0.7 0.9]
交叉熵 cHpq: 0.22907268296853875
二元交叉熵 bcHpq: 0.4003674356962309在该示例中,二元交叉熵 bcHpq 显示了预测分布与真实分布之间的差异大小。从结果可见,二元交叉熵大约是交叉熵 cHpq 的两倍,这说明它是一种增强的差异度量。
这种增大很重要,因为二元交叉熵不仅考虑事件发生的概率,也考虑事件不发生的概率,因此在二分类问题中评价预测质量时更加敏感。
4.6.3 示例 3:较均匀真实分布下的二元交叉熵:优质预测
在本例中,真实概率分布更加均匀,这可能意味着事件发生与不发生的机会大致相等。在这种情况下,优质预测也应当给出接近真实分布的、较均匀的概率分布。此时二元交叉熵会较低,表明预测分布与真实分布匹配良好。
假设我们要预测一次公平硬币投掷会出现正面还是反面。每种结果的真实概率均为 50%(0.5)。如果模型预测正面出现的概率为 52%(0.52),反面为 48%(0.48),那么预测较为均匀,也接近真实分布。此时二元交叉熵相对较低,说明预测质量较好。
在前两个示例中,我们考虑了真实分布较为极端的情况,其概率为 1.0 和 0。在这两个示例中,我们观察到使用二元交叉熵能够得到一种增强的熵度量。在本例中,我们考虑更加均匀的真实分布,并研究二元交叉熵的行为。
考虑如下较均匀的预测分布和真实分布:
import numpy as np
# 预测分布
q = np.array([0.4, 0.2, 0.3, 0.1]) # 预测
# 真实分布
p = np.array([0.3, 0.3, 0.2, 0.2]) # 真实值,较均匀的分布
# 计算用于二元交叉熵的预测和真实反向值
p_conv = 1.0 - p # 真实值的反面
q_conv = 1.0 - q # 预测值的反面
# 输出真实分布、预测分布及其反向值
print("真实:", p, "预测:", q, ' 反向值: 真实:', p_conv, '预测:', q_conv)
# 计算普通交叉熵
cHpq = -np.sum(p * np.log(q)) / len(p)
# 计算二元交叉熵
bcHpq = -np.sum(p * np.log(q) + p_conv * np.log(q_conv)) / len(p)
# 输出交叉熵结果
print('交叉熵 cHpq:', cHpq)
print('二元交叉熵 bcHpq:', bcHpq)输出结果:
真实: [0.3 0.3 0.2 0.2] 预测: [0.4 0.2 0.3 0.1] 反向值: 真实: [0.7 0.7 0.8 0.8] 预测: [0.6 0.8 0.7 0.9]
交叉熵 cHpq: 0.36475754318911824
二元交叉熵 bcHpq: 0.585609240747465在这段代码中,我们首先定义预测概率分布 q 和真实概率分布 p。然后计算这些分布的反向值 p_conv 和 q_conv,它们是计算二元交叉熵所必需的。随后计算并输出两个熵指标:普通交叉熵 cHpq 和二元交叉熵 bcHpq。
在这种情况下也可以观察到,二元交叉熵并没有带来明显改进。这说明,虽然二元交叉熵在某些情形下更有信息量,但它并不总是能更好地区分预测分布和真实分布,尤其是当真实分布更加均匀时。
4.6.4 示例 4:较均匀真实分布下的二元交叉熵:预测质量
这里同样处理较均匀的真实概率分布,但预测与该分布存在显著差异。例如,如果真实分布表示事件发生与不发生的概率相等,而预测强烈偏向某一个结果,那么二元交叉熵会较高,表示预测质量较差。
回到硬币示例,假设模型预测正面出现的概率为 80%(0.8),反面为 20%(0.2)。这时,预测显著偏离真实的均匀概率分布(正面 50%、反面 50%)。因此,二元交叉熵会较高,说明预测质量较差,因为它不符合预期的均匀概率分布。
在前两个示例中,我们考虑的是极端真实分布:概率分别为 1.0 和 0。对于这两个示例,我们使用二元交叉熵观察到了增强的熵度量。在本例中,我们考虑更加均匀的真实分布,并研究二元交叉熵的行为。
在这个示例中,预测分布 q 和真实分布 p 都相对均匀,这使我们能够更细致地研究二元交叉熵作为预测准确性度量的有效性。
import numpy as np
# 预测分布
q = np.array([0.4, 0.2, 0.3, 0.1]) # 预测
# 真实分布
p = np.array([0.3, 0.3, 0.2, 0.2]) # 真实值,较均匀的分布
# 计算预测和真实分布的反向值
p_conv = 1.0 - p # 真实值的反面
q_conv = 1.0 - q # 预测值的反面
# 输出数值以便观察
print("真实:", p, "预测:", q, ' 反向值:', p_conv, q_conv)
# 计算交叉熵
cHpq = -np.sum(p * np.log(q)) / len(p)
# 计算二元交叉熵
bcHpq = -np.sum(p * np.log(q) + p_conv * np.log(q_conv)) / len(p)
# 输出结果
print('交叉熵 cHpq:', cHpq)
print('二元交叉熵 bcHpq:', bcHpq)输出结果:
交叉熵 cHpq: 0.36475754318911824
二元交叉熵 bcHpq: 0.585609240747465在该情况下,我们同样发现二元交叉熵并没有带来改进。这说明,尽管二元交叉熵可以作为评估预测质量的有用度量,尤其是在真实分布具有极端概率值时,但在较均匀分布的情况下,它的有效性可能并不那么明显。
通过考察不同示例——从优质预测到较差预测,从均匀真实分布到非均匀真实分布——可以看到二元交叉熵如何帮助判断模型对事件发生概率的预测效果。
在天气预测和抛硬币的示例中,我们看到,较低的二元交叉熵表明预测概率分布接近真实分布,说明模型质量较高。相反,较高的二元交叉熵表示预测概率与真实概率之间存在显著差异,这是预测质量较差的标志。
4.7 贝叶斯统计
贝叶斯统计是一种根据已有数据来评估事件概率的方法。为了用更直观的方式说明,可以设想一个糖果盒,其中装有红色和蓝色糖果,但两种颜色的比例未知。如果我们不断从盒中取出糖果,并发现大多数都是红色,就可能推断盒中红色糖果占优势。然而,如果后来得知盒中最初红色和蓝色糖果数量相同,那么对概率的估计就会发生变化:抽到较多红色糖果也可能只是随机结果。贝叶斯统计可以对这类情形进行数学分析,并在获得新数据后不断修正原先的假设。这种方法广泛用于改进预测和基于已有信息作出决策,因此在科学和实践应用中都是非常有价值的工具。
让我们用 Python 检验这个想法。下面创建一个简单程序,它从“盒子”中“取出”糖果,并帮助我们更新关于每种颜色糖果数量的假设。
import random
# 每种颜色糖果的初始数量
red = 50
blue = 50
# 模拟选择糖果的函数
def choose_candy():
global red, blue
if random.random() < red / (red + blue):
red -= 1
return 'red'
else:
blue -= 1
return 'blue'
# 模拟选择 10 颗糖果
for _ in range(10):
print(choose_candy())
# 查看剩余糖果数量如何变化
print(f"剩余红色糖果数量: {red}")
print(f"剩余蓝色糖果数量: {blue}")这个例子有助于理解贝叶斯统计的工作方式:我们从一个初始假设出发,并在获得新信息后更新它;在本例中,新信息就是从盒子中取出的糖果颜色。
考虑一个包含多个随机变量并且这些变量共同出现的统计事件。当处理多个随机变量时,我们通常关注联合概率,即对于给定元素,事件 A 和 B 同时发生的概率。显然,对于任意取值,P(A,B)\leq P(A) 且 P(A,B)\leq P(B),因为 A 的测量与 B 是否发生无关。为了使事件 A 和 B 共同发生,必须同时发生 A 和 B。因此,A 与 B 联合发生的概率不可能高于其中任一事件单独发生的概率。
在已知 A 的条件下,联合概率可表示为条件概率,记作 P(B|A),它表示在 A 已经发生的条件下 B 发生的概率。这将我们引向重要的贝叶斯定理。
根据定义,有:
\[ P(A,B)=P(B|A)P(A) \]
由对称性也有:
\[ P(A,B)=P(A|B)P(B) \]
因此得到贝叶斯公式:
\[ P(A|B)=\frac{P(B|A)P(A)}{P(B)} \]
# 初始化概率
p_A = 0.3 # 事件 A 的概率
p_B_given_A = 0.6 # 在 A 发生条件下 B 发生的概率
p_B = 0.2 # 事件 B 的概率
# 根据贝叶斯定理计算在 B 发生条件下 A 发生的条件概率
p_A_given_B = (p_B_given_A * p_A) / p_B
# 输出结果
print(f"在 B 条件下 A 的条件概率: {p_A_given_B:.2f}")输出:
在 B 条件下 A 的条件概率: 0.90其中,p_A 表示事件 A 发生的先验概率,即在获得任何数据之前的概率。
p_B_given_A 是在已知事件 A 已经发生的条件下,事件 B 发生的概率。
p_B 是事件 B 的先验概率。
p_A_given_B 使用贝叶斯定理计算得到,表示在事件 B 已经发生的条件下,事件 A 发生的条件概率。
4.7.1 使用朴素贝叶斯方法进行分类
朴素贝叶斯分类是一种基于统计的学习方法,用于根据先前学习到的数据确定新数据所属的类别。这种方法被称为“朴素”,是因为它假设数据中的所有特征彼此独立,而这在现实中并不总是成立。
首先,需要收集已经标注好的数据,其中每个数据项都明确属于某一类别。这样算法就可以学习与每个类别相关的特征。
下一步,算法会根据已经收集的数据,计算每个类别中特定特征出现的概率。例如,如果某个类别中的大多数对象都具有某一特征,那么算法就会认为新对象出现这一特征是其属于该类别的证据。
当向算法提供一个带有一组特征的新对象时,它会利用此前计算的概率来判断该对象最可能属于哪个类别。算法会评估每个类别的概率,并选择概率最大的类别。
朴素贝叶斯算法因其简单性和快速处理大量数据的能力而适合分类任务。然而,它的“朴素性”——也就是特征独立性假设——在特征之间存在相关性的复杂任务中可能成为限制。
安装 scikit-learn 库:
pip install scikit-learn下面这些 Python 库常用于机器学习和文本处理任务,各自具有不同用途:
from sklearn.model_selection import train_test_split:scikit-learn 中用于将数据集划分为训练集和测试集的函数。它可以在模型未见过的数据上评估模型性能,从而检验模型的泛化能力。
from sklearn.feature_extraction.text import CountVectorizer:用于将文本数据转换为数值形式,生成所谓的“词袋”(bag of words)。它统计训练集中每个词在每篇文档中的出现次数,并把文本转换为可用于机器学习的特征向量。
from sklearn.naive_bayes import MultinomialNB:用于多项式特征的朴素贝叶斯算法,经常用于文本分类。该算法基于贝叶斯定理,并假设每个类别内部的特征彼此独立。它非常适合处理经过 CountVectorizer 转换后的向量化文本数据。
from sklearn.metrics import accuracy_score:用于评估机器学习模型质量的函数。accuracy_score 计算分类准确率,即正确预测的标签数在全部预测中的比例。准确率是最简单且最常用的分类模型性能指标之一。这些工具可以结合起来创建并评估机器学习模型,尤其适用于文本分类。例如,可以用它们开发一个自动把电子邮件划分为垃圾邮件和非垃圾邮件的系统。
# 导入必要的库
from sklearn.model_selection import train_test_split # 用于将数据划分为训练集和测试集
from sklearn.feature_extraction.text import CountVectorizer # 用于通过向量化把文本转换为数值形式
from sklearn.naive_bayes import MultinomialNB # 用于创建和使用朴素贝叶斯分类器
from sklearn.metrics import accuracy_score # 用于计算模型预测准确率
# 文本消息示例及其对应标签(1 表示垃圾邮件,0 表示非垃圾邮件)
messages = [
"бесплатные билеты отправьте СМС",
"встретимся на обеде",
"специальное предложение купите сейчас",
"привет как дела",
"не упустите шанс получить скидку"
]
labels = [1, 0, 1, 0, 1]
# 将数据划分为训练集和测试集
messages_train, messages_test, labels_train, labels_test = train_test_split(
messages, labels, test_size=0.4, random_state=42
)
# 文本数据向量化
vectorizer = CountVectorizer()
messages_train_vectorized = vectorizer.fit_transform(messages_train)
messages_test_vectorized = vectorizer.transform(messages_test)
# 训练朴素贝叶斯分类器
classifier = MultinomialNB()
classifier.fit(messages_train_vectorized, labels_train)
# 在测试数据上进行预测
predictions = classifier.predict(messages_test_vectorized)
# 评估模型准确率
accuracy = accuracy_score(labels_test, predictions)
print(f"模型准确率: {accuracy}")输出:
模型准确率: 0.5模型准确率为 0.5 表明,在两个测试样本中,模型只正确分类了一个。对于非常小的数据集来说,这是可以预期的结果,因为机器学习模型通常难以从数量有限的训练样本中归纳出可靠规律。
在这个例子中,将 test_size 增加到 0.4 使测试集中包含两个样本,这让我们能稍微更多地观察模型在新数据上的表现。然而,50% 的准确率说明,如果有更多且更加多样化的训练数据,模型的结果会显著改善。
4.7.2 问题表述
基于贝叶斯统计,人们发展出一种流行算法,即朴素贝叶斯分类器。考虑一个包含 p 个变量 x_1,x_2,\ldots,x_p 的事件。我们假设任意变量 x_i 都与其他变量相互独立。对于给定标签 y,x 的条件概率可表示为:
\[ P(x|y)=\prod_{i=1}^{p}P(x_i|y) \]
根据贝叶斯定理,有:
\[ P(y|x)=\frac{P(x|y)P(y)}{P(x)} \]
虽然我们可能不知道 P(x),也就是事件 x 发生的概率,但在计算 P(y|x) 时它通常不是必需的,因为它只起到归一化作用。因此,可以改用如下形式:
\[ P(y|x)\propto P(y)\prod_{i=1}^{p}P(x_i|y) \]
4.7.3 机器学习方法在手写数字识别中的分析
现在可以考察如何编写朴素贝叶斯分类器来识别手写数字。这里使用著名的 MNIST 数据集来训练该分类器。MNIST 数据集总共包含 70,000 张手写数字图像,其中 60,000 张用于训练,10,000 张用于测试;数字类别为 0 到 9,并且所有图像都有标签。这些图像来自美国人口普查局工作人员和美国中学生。
数字分类任务可以转化为计算给定图像 x 属于某个数字 y 的概率,即 P(y|x)。任意图像 x 都包含像素 x_i,每个像素可以取值为 1(开启)或 0(关闭),因此可以视为二元变量。
随后可以使用前面的公式,其中需要估计 P(y) 和 P(x_i|y)。二者都可以通过 MNIST 训练集对每个数字进行计算。例如,在 MNIST 训练集的 60,000 张图像中,如果数字 4 出现 5,800 次,那么就可估计 P(y=4)=5800/60000。为了估计 P(x_i|y),每个像素 x_i 都是二元的,并且 P(x_i=0|y)=1-P(x_i=1|y)。估计方法是统计在标签为 y 的图像中该像素被点亮的次数,然后除以标签为 y 的图像总数。在这个简单算法中,训练所需做的事情就是统计 MNIST 训练图像中每个标签的数量。
开始使用 MNIST 前,应确保 Python 已安装证书,以避免出现 SSL: CERTIFICATE_VERIFY_FAILED 这类错误。
要解决 CERTIFICATE_VERIFY_FAILED 问题,需要为所使用的 Python 环境安装 SSL 证书。如果使用 macOS 自带的 Python,或从 python.org 安装的 Python,通常可以通过运行随 Python 安装包提供的 InstallCertificates.command 脚本解决问题。
1. 打开 Finder,进入 Python 安装目录。如果你从 python.org 安装 Python,它可能位于类似 /Applications/Python 3.x 的目录中,其中 3.x 对应你安装的 Python 版本。
2. 在 Python 目录中找到名为 InstallCertificates.command 的文件。
3. 双击 InstallCertificates.command 文件运行脚本。这会打开终端窗口并为 Python 环境安装必要的 SSL 证书。运行该脚本后,再次执行代码。此时应当可以在不出现 SSL 证书验证错误的情况下加载 MNIST 数据集。
如果使用虚拟环境,或使用并非来自 python.org 的 Python 安装方式,可能需要确认 SSL 证书对该 Python 环境可用。有时可以通过 pip 安装 certifi 包来实现:
pip install certificertifi 包包含用于验证 TLS 主机身份时检查 SSL 证书可信性的根证书集合。不过该方法的效果取决于你的 Python 环境以及它处理 SSL 证书的配置方式。
在 Windows 中修复 CERTIFICATE_VERIFY_FAILED 错误时,需要确保 Python 环境能够访问必要的 SSL 证书。下面是 Windows 用户的处理步骤:
1. 以管理员身份打开命令提示符(Command Prompt)或 PowerShell。可以点击“开始”,输入 “cmd” 或 “PowerShell”,然后右键相应程序并选择“以管理员身份运行”。
2. 找到 Python 安装目录。如果你从 python.org 官方网站安装 Python,目录通常类似:C:\Users\你的用户名\AppData\Local\Programs\Python\Python3X,其中 Python3X 是你安装的 Python 版本。如果不确定安装位置,可在命令行中执行 where python 查找。
3. 使用 cd 命令进入 Python 安装目录,例如:
cd C:\Users\你的用户名\AppData\Local\Programs\Python\Python3X
4. 在该目录中运行证书安装或更新命令:
.\Scripts\pip.exe install --upgrade certifi
该命令会安装或更新 certifi 包,其中包含一组根证书。
5. 安装或更新 certifi 后,再次执行你的代码。在 Ubuntu 的 Python 环境中解决 SSL 证书 CERTIFICATE_VERIFY_FAILED 问题,可按以下步骤操作:
1. 打开终端。可以按 Ctrl + Alt + T。
2. 更新软件包列表并安装系统更新,确保系统包处于最新状态:sudo apt update
sudo apt upgrade3. 安装 ca-certificates 与 openssl 包(如果尚未安装):sudo apt install ca-certificates openssl4. 如果使用通过 apt 包管理器安装的 Python,证书通常已经配置好。如果 Python 通过其他方式安装,可能需要手动安装证书。
5. 若要为 Python 环境安装或更新证书,可执行:sudo /usr/local/bin/python3 -m pip install --upgrade certifi如果使用的 Python 解释器路径不同,请将 /usr/local/bin/python3 替换为实际路径。
执行这些命令后,重新运行 Python 代码。它应当能够正确处理 SSL 连接,而不会出现证书验证错误。
如果使用 pyenv 或 virtualenv 创建的 Python 环境,也要确保这些环境能够访问证书,因为它们可能使用自己的证书集合。在这种情况下,需要在每个虚拟环境中安装 certifi:
pip install --upgrade certifi整个训练过程可以归结为计数:
import tensorflow as tf
import numpy as np
# 使用 TensorFlow 加载 MNIST 数据
mnist = tf.keras.datasets.mnist
(training_images, training_labels), _ = mnist.load_data()
# 图像归一化
training_images = training_images / 255.0
# 初始化概率计数器
num_labels = 10 # 标签数量(0 到 9)
pixel_count = 28 * 28 # MNIST 图像尺寸 28x28
p_y = np.zeros(num_labels) # 每个数字的概率
p_xi_given_y = np.zeros((num_labels, pixel_count)) # 每个数字下每个像素的概率
# 计数以估计概率
for label in training_labels:
p_y[label] += 1
for i, image in enumerate(training_images):
label = training_labels[i]
# 将图像转换为二值向量
binary_image = np.round(image).flatten()
p_xi_given_y[label] += binary_image
# 归一化得到概率
p_y /= len(training_labels)
p_xi_given_y /= np.sum(p_xi_given_y, axis=1, keepdims=True)
# 在取对数前为概率添加小常数 epsilon
epsilon = 1e-9
p_y += epsilon
p_xi_given_y += epsilon
# 对新图像进行分类的函数
def classify(image):
binary_image = np.round(image).flatten()
# 计算给定图像下每个数字的对数概率
log_prob_y = np.log(p_y) + np.dot(binary_image, np.log(p_xi_given_y.T))
# 返回概率最大的数字
return np.argmax(log_prob_y)
# 用一张图像测试分类器
test_image = training_images[0]
predicted_digit = classify(test_image)
print("预测数字:", predicted_digit)输出:
预测数字: 54.7.4 基于朴素贝叶斯的分类算法
# 导入必要的库
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
# 使用 TensorFlow Keras 加载 MNIST 数据集
(mnist_train_images, mnist_train_labels), (mnist_test_images, mnist_test_labels) = tf.keras.datasets.mnist.load_data()
# 图像转换函数
def transform(images, labels):
# 数据转换:当像素值 >= 128 时认为像素“开启”
transformed_images = np.floor(images / 128).astype(np.float32)
return transformed_images, labels.astype(np.float32)
# 转换训练集和测试集
train_images, train_labels = transform(mnist_train_images, mnist_train_labels)
test_images, test_labels = transform(mnist_test_images, mnist_test_labels)
# 显示数据集中的一个图像示例
image_index = 8888
plt.imshow(train_images[image_index], cmap='Greys')
plt.title(f'Label: {train_labels[image_index]}')
plt.show()
# 初始化用于统计概率的数组
ycount = np.ones(10) # 对应 10 个数字
xcount = np.ones((784, 10)) # 对应每个数字的 784 个像素
# 处理训练数据集
for image, label in zip(train_images, train_labels):
x = image.reshape((784,))
y = int(label)
ycount[y] += 1
xcount[:, y] += x
# 计算概率 p(xi|y)
for i in range(10):
xcount[:, i] = xcount[:, i] / ycount[i]
# 计算 p(y)
py = ycount / np.sum(ycount)
# 显示“训练后”的模型
fig, figarr = plt.subplots(1, 10, figsize=(15, 15))
for i in range(10):
figarr[i].imshow(xcount[:, i].reshape((28, 28)), cmap='hot')
figarr[i].axes.get_xaxis().set_visible(False)
figarr[i].axes.get_yaxis().set_visible(False)
plt.show()
# 输出每个数字的概率
print(py)输出示例:
[0.09871688 0.11236461 0.09930012 0.10218297 0.09736711 0.09035161
0.09863356 0.10441593 0.09751708 0.09915014]图:MNIST 数据集中一个手写数字样本图像。
图:手写数字的平均外观。
这段代码使用 TensorFlow 加载 MNIST 训练集和测试集,将图像转换为二值形式:像素值为 128 及以上时认为像素“开启”。然后,代码基于训练集计算每个数字的概率,以及每个数字中每个像素被点亮的概率。最后,它可视化每个数字对应的概率分布,这些分布表示基于训练数据得到的每个数字的平均外观。
4.7.5 朴素贝叶斯模型测试
现在使用 MNIST 测试集研究这个基于统计的模型性能。前面完成的“训练”本质上只是对训练集进行计数,它给出了 P(y) 和 P(x_i|y)。对于测试集中的给定图像 x,需要计算其对应于标签 y 的概率,即使用前述公式计算 P(y|x),其中 P(x|y) 由训练好的模型计算得到。为了避免连续乘以大量很小的概率数值,我们改为计算如下对数形式,即“对数概率”:

\[ \log P(y|x) \propto \log P(y)+\sum_i \log P(x_i|y) \]
对于给定图像 x,特征 x_i 是二元的,只能取 1 或 0。由于我们使用训练好的模型计算概率,因此有:

\[ P(x_i|y)= \begin{cases} P(x_i=1|y), & x_i=1,\\ 1-P(x_i=1|y), & x_i=0. \end{cases} \]
通过一个数学技巧,上式可以写成统一形式:

\[ P(x_i|y)=P(x_i=1|y)^{x_i}\left(1-P(x_i=1|y)\right)^{1-x_i} \]
这是用训练模型预测正变量概率时,计算二元变量事件概率的一般公式。最终得到:
\[ \log P(y|x) \propto \log P(y)+\sum_i \left[x_i\log P(x_i=1|y)+(1-x_i)\log(1-P(x_i=1|y))\right] \]
由此可见,测试过程本质上是在测量给定图像的分布(真实分布)与由数据集计算出的带标签平均图像分布(模型分布)之间的二元交叉熵。因此可以直接使用二元交叉熵公式写出相应计算。
为了避免重复计算对数,预先计算所有 y 的 \log P(y),以及所有像素的 \log P(x_i|y) 和 \log(1-P(x_i|y))。
# 导入用于数据处理和可视化的必要库
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
# 加载 MNIST 数据集,其中包含用于训练机器学习模型的手写数字
(mnist_train_images, mnist_train_labels), (mnist_test_images, mnist_test_labels) = tf.keras.datasets.mnist.load_data()
# 图像预处理函数
def transform(images, labels):
"""
将原始图像转换为二值图像(0 或 1)。
如果像素值 >= 128,则认为像素为“开启”(1);否则为“关闭”(0)。
输入图像和标签,返回转换后的图像和标签。
"""
transformed_images = np.floor(images / 128).astype(np.float32)
return transformed_images, labels.astype(np.float32)
# 对训练集和测试集应用转换函数
train_images, train_labels = transform(mnist_train_images, mnist_train_labels)
test_images, test_labels = transform(mnist_test_images, mnist_test_labels)
# 可视化转换后的一个图像示例
image_index = 8888
plt.imshow(train_images[image_index], cmap='Greys')
plt.title(f'标签: {int(train_labels[image_index])}')
plt.show()
# 初始化用于统计概率的数组
ycount = np.ones(10) # 初始值 1 可避免除零(拉普拉斯平滑)
xcount = np.ones((784, 10)) # 同样用于避免除零
# 处理训练集以训练朴素贝叶斯分类器
for image, label in zip(train_images, train_labels):
x = image.reshape((784,))
y = int(label)
ycount[y] += 1
xcount[:, y] += x
# 归一化得到概率 p(xi|y)
for i in range(10):
xcount[:, i] /= ycount[i]
# 计算每个数字的先验概率 p(y)
py = ycount / np.sum(ycount)
# 可视化“训练后”的模型:显示每个数字下每个像素“开启”的概率
fig, figarr = plt.subplots(1, 10, figsize=(15, 15))
for i in range(10):
figarr[i].imshow(xcount[:, i].reshape((28, 28)), cmap='hot')
figarr[i].axis('off')
plt.show()
# 在测试图像上计算并可视化每个数字的概率
logxcount = np.log(xcount)
logxcountneg = np.log(1 - xcount)
logpy = np.log(py)
fig, figarr = plt.subplots(2, 10, figsize=(15, 3))
ctr = 0
y_true = []
pxm = np.array([])
xi_pred = []
# 在测试图像上评估模型
for image, label in zip(test_images, test_labels):
x = image.reshape((784,))
y_true.append(int(label))
logpx = logpy.copy()
for i in range(10):
# 计算联合概率 P(x|y)P(y) 的对数
logpx[i] += np.dot(logxcount[:, i], x) + np.dot(logxcountneg[:, i], 1 - x)
logpx -= np.max(logpx) # 归一化以防止数值溢出
px = np.exp(logpx)
px *= py
px /= np.sum(px)
pxm = np.append(pxm, np.max(px))
xi_pred.append(np.argmax(px))
figarr[1, ctr].bar(range(10), px, color='blue')
figarr[1, ctr].axis('off')
figarr[0, ctr].imshow(x.reshape((28, 28)), cmap='hot')
figarr[0, ctr].axis('off')
ctr += 1
if ctr == 10:
break
plt.show()
# 输出分类结果
y_true = np.array(y_true)
xi_pred = np.array(xi_pred)
print('真实标签: ', y_true)
print('预测数字:', xi_pred)
print('是否正确?', np.equal(y_true, xi_pred))
print('最大概率:', pxm)输出示例:
真实标签: [7 2 1 0 4 1 4 9 5 9]
预测数字: [7 2 1 0 4 1 4 9 4 9]
是否正确? [ True True True True True True True True False True]
最大概率: [1. 1. 1. 1. 1. 1.
0.99999999 0.9999996 0.99999758 0.99999899]这段代码实现了一个用于识别 MNIST 手写数字的朴素贝叶斯分类器,使用 NumPy、TensorFlow 和 Matplotlib 进行数据处理与可视化。它加载 MNIST 数据集,将图像转换为二值格式(根据像素强度判断像素“开启”或“关闭”),然后通过统计每个数字中“开启”像素的出现概率以及每个数字在训练集中的概率来训练模型。训练完成后,模型在测试集上进行测试:对于每张图像,计算它属于每个类别(数字)的概率,选择概率最大的类别,并将预测结果及其概率与真实标签一起可视化。
4.9 结论
测试结果表明,所给分类器在测试集前十个数字中有一个分类错误。具体来说,第九个数字本应识别为“5”,却被错误地分类为“4”。值得注意的是,分类器对这一错误预测的置信度很高,接近 1。这表明分类模型背后的某些假设可能存在不足。其中一个假设是:图像中的每个像素都仅根据类别标签独立生成。然而现实要复杂得多,因为数字图像是一种复杂函数,其中包含像素之间的相互关系。在这一背景下,单一统计信息的局限性说明,对于图像分类任务,需要使用更高级的方法。
类似这里讨论的朴素贝叶斯分类器在 20 世纪 80 年代和 90 年代曾非常流行,尤其是在垃圾邮件过滤等应用中。然而,在现代图像处理中,它们逐渐让位于更强大的方法,例如卷积神经网络(CNN)。
另一种方法是使用交叉熵或二元交叉熵来评估预测质量。利用训练数据集,可以为每个图像和类别标签计算概率 P(x|y),其中类别标签可取 0 到 9。然后,对于测试图像与每个可能类别对应的概率分布,计算交叉熵,并把交叉熵最小的类别确定为该测试图像最可能的类别标签。
这个例子说明了统计分析在分类任务和机器学习整体中的价值,同时也展示了如何为特定数据集计算统计指标。尽管存在一些限制,朴素分类器仍然是机器学习工具箱中的有力工具,尤其适用于不存在严格物理规律的任务。它们广泛应用于医疗应用、推荐系统、文本分类以及基于真实数据的预测。训练朴素贝叶斯分类器可以使用 Scikit-learn 库,该库提供了方便且灵活的 sklearn.naive_bayes 模块。此外,在开发更复杂的机器学习模型(包括深度神经网络)时,TensorFlow 也被广泛使用。TensorFlow 提供了丰富的工具和 API,可用于设计、训练和部署从简单到复杂的深度学习模型,因此是 Scikit-learn 在解决更复杂机器学习任务时的重要补充。



自测
哪一项最能概括“基于统计与概率的学习模型”这一章的重点?
在机器学习中,理论定义需要通过数值示例和可视化进行检验。
哪些做法有助于巩固本章内容?
参加测试