因果人工智能——因果关系与深度学习的连接

本章内容包括:

  • 将深度学习整合进因果图模型
  • 使用变分自编码器训练因果图模型
  • 利用因果方法提升机器学习效果

本书名为《因果人工智能(Causal AI)》,但因果性究竟如何与人工智能相连?更具体地说,因果性如何与人工智能主流范式------深度学习相连?本章将从两个视角探讨这一问题:

  1. 如何将深度学习融入因果模型------我们将以计算机视觉问题的因果模型为例(第5.1节),随后训练深度因果图像模型(第5.2节)。
  2. 如何利用因果推理优化深度学习------我们将通过机制独立性和半监督学习案例研究(第5.3.1和5.3.2节),并通过因果视角揭示深度学习的本质(第5.3.3节)。

"深度学习"广义上指深度神经网络的应用。它是一种机器学习方法,通过层层堆叠非线性模型,模拟大脑神经元的连接结构。"深度"指堆叠多层以提升模型表达能力,特别是在建模高维非线性数据(如视觉媒体和自然语言文本)方面的优势。神经网络已经存在多年,近年来硬件和自动微分技术的进步使得深度神经网络能够扩展到极大规模。这种扩展能力使得深度学习在图像识别、自然语言处理、游戏、医疗诊断、自动驾驶以及逼真文本、图像和视频生成等多项高级推理和决策任务中,多次超越人类表现。

然而,探讨深度学习与因果性的关联时,常会得到令人沮丧的回答。部分AI公司CEO和科技巨头领导者炒作深度学习模型的强大,甚至宣称它们能学习世界的因果结构;而部分顶尖研究者则批评这些模型只是"随机鹦鹉",只能模仿复杂的相关模式,却未能真正理解因果关系。

本章目标是调和这些观点。总结来看,深度学习架构可以被整合进因果模型,并利用深度学习的训练方法对其进行训练;同时,我们也可以运用因果推理设计更优的深度学习模型,并改进其训练方式。

我们将以两个案例来支撑这一观点:

  • 使用变分自编码器构建计算机视觉的因果DAG
  • 利用机制独立性实现更优的半监督学习

本书其他章节中,因果与人工智能的交叉示例都会基于这些案例所建立的直觉。例如,第9章将展示如何用变分自编码器实现反事实推理,第11章探讨机器学习和概率深度学习在因果效应推断中的应用,第13章则讲述如何结合大语言模型和因果推理。

我们将先从如何将深度学习融入因果模型开始。

5.1 计算机视觉问题的因果模型

让我们来看一个可以用因果DAG来处理的计算机视觉问题。回想一下第1章介绍的MNIST数据集,它由数字图像及其对应标签组成,如图5.1所示。

有一个相关的数据集叫做 Typeface MNIST(简称 TMNIST),它同样包含数字图像及其对应的数字标签。但不同的是,TMNIST 中的数字不是手写体,而是以2,990种不同字体渲染的数字,如图5.2所示。对于每张图像,除了数字标签外,还有一个字体标签。字体标签的示例包括"GrandHotel-Regular"、"KulimPark-Regular"和"Gorditas-Bold"。

在本分析中,我们将把这两个数据集合并为一个,并在该数据上构建一个简单的深度因果生成模型。我们会将"字体"标签简化为一个二元标签,对 MNIST 图像标记为"手写",对 TMNIST 图像标记为"印刷"。

我们已经了解了如何基于 DAG 构建因果生成模型。我们将联合分布分解为多个因果马尔可夫核的乘积,每个核代表一个节点在其父节点条件下的条件概率分布。在之前的 pgmpy 示例中,我们为每个核拟合了条件概率表。

你可以想象,用条件概率表来表示图像中像素的条件概率分布是多么困难。但没有什么阻止我们用深度神经网络来建模因果马尔可夫核,而深度神经网络足够灵活,可以处理像素这样的高维特征。本节将演示如何用深度神经网络来建模由因果 DAG 定义的因果马尔可夫核。

5.1.1 利用通用函数逼近器

深度学习是一个极其高效的通用函数逼近器。设想有一个函数,将一组输入映射到一组输出,但我们不知道该函数,或其数学表达或编码实现过于复杂。只要有足够多的输入输出样本,深度学习就能高精度地逼近该函数。即使该函数是非线性且高维的,只要数据充足,深度学习也能学习出良好的近似。

在因果建模和推断中,我们经常需要处理函数,有时对它们进行逼近是合理的,只要逼近保持我们关注的因果信息。例如,因果马尔可夫性质使我们关注将因果DAG中节点父节点的取值映射到该节点的取值(或概率取值)的函数。

本节将用变分自编码器(VAE)框架实现节点与其父节点之间的映射。我们将在VAE中训练两个深度神经网络:一个将父节点变量映射到结果变量的分布,另一个将结果变量映射到父节点变量的分布。这个例子展示了当因果关系是非线性和高维时,深度学习的应用;其中因变量是表示为高维数组的图像,自变量代表图像内容。

5.1.2 因果抽象与板模型

那么,构建图像的因果模型意味着什么?图像由按网格排列的像素组成。作为数据,我们可以将该像素网格表示为对应颜色数值的矩阵。对于 MNIST 和 TMNIST,图像均为 28 × 28 的灰度矩阵,如图5.3所示。

图5.3 显示了一个"6"的MNIST图像(左)和一个"7"的TMNIST图像。它们的原始形式都是28 × 28的矩阵,矩阵中的数值对应灰度值。

典型的机器学习模型会将这个28 × 28的像素矩阵视为784个特征。机器学习算法学习像素之间以及像素与标签之间的统计模式。基于这一点,可能有人会倾向于将每个像素都视为朴素因果DAG中的一个节点,如图5.4所示。为了视觉上的简洁,我只绘制了16个像素(一个任意数量),而非全部784个。

图5.4 展示了用4 × 4矩阵表示的图像可能对应的朴素因果DAG的样子。

在图5.4中,数字变量和"是否手写"变量分别向每个像素节点发出边。此外,图中还展示了像素间可能存在的因果关系边。像素之间的因果边意味着一个像素的颜色可能是另一个像素颜色的原因。这些因果关系大多存在于相近的像素节点之间,但也可能有少量远距离的连接。但我们怎么知道一个像素是否真的影响另一个像素呢?如果两个像素间有连边,我们又如何判断因果方向?

在合适的抽象层级工作

即使只有16个像素,图5.4中的朴素DAG已显得相当复杂。若是784个像素,情况会更糟。除了结构复杂难以操作之外,像素级模型的问题在于,我们的因果问题通常不是在像素层面思考------我们几乎不会问"这个像素对那个像素有什么因果效应?"换言之,像素层级的抽象太低,这也是为什么思考像素间的因果关系感觉有些荒谬。

在应用统计领域,如计量经济学、社会科学、公共卫生和商业,我们的数据变量通常是人均收入、收入总额、地理位置、年龄等。这些变量通常就是我们想要思考的抽象层级。但现代机器学习关注的是来自原始媒体(图像、视频、文本和传感器数据)的感知问题,我们通常不希望在这些低级特征层面进行因果推理。我们的因果问题通常关心的是这些低级特征背后的高级抽象,我们需要在更高的抽象层级建模。

与其关注单个像素,我们更关注整张图像。我们定义变量 X 来表示图像的外观,即 X 是一个代表像素的矩阵随机变量。图5.5展示了TMNIST案例的因果DAG。简单来说,数字身份(0到9)和字体(2990种可能)是原因,图像是结果。

在这个例子中,我们使用因果DAG来断言标签(数字类别)是图像的原因。但情况并非总是如此,正如我们将在第5.3节的半监督学习案例研究中讨论的那样。和所有因果模型一样,这取决于领域内的数据生成过程(DGP)。

为什么说数字是图像的原因

柏拉图的"洞穴寓言"描述了一群人一生都生活在洞穴里,从未见过外面的世界。他们面向空白的洞穴墙壁,观看墙上投射的火光前物体的影子。这些影子是物体的简化甚至有时是扭曲的表现。在这里,我们可以把物体的真实形态看作是影子的原因。

类似地,数字标签的真实形态是图像表现的原因。MNIST图像是人写出来的,写作者脑中有数字的柏拉图式理想形态,想要表现到纸上。在这个过程中,手的动作变化、纸张的角度、笔与纸的摩擦等因素扭曲了理想,最终呈现的图像就是那个"理想"投射的"影子"。

这个观点与计算机视觉中的"逆向图形学"(vision as inverse graphics)概念相关(详见 www.altdeep.ai/p/causalaib...)。从因果角度来看,分析环境中原始信号渲染出的图像时,推断导致信号的真实物体或事件,因果关系是从物体或事件流向信号。推断任务是用观察到的信号(洞穴墙上的影子)推断原因(火光前的物体)的本质。

话虽如此,图像也可以是原因。例如,如果你在建模用户在手机应用中看到一张图像后的行为(如点击、点赞或向左滑动),那么图像可以作为行为的原因。

板模型(Plate Modeling)

在我们这里,针对TMNIST中2990种字体进行建模显得过于复杂。因此,我将两个数据集合并为一个------一半来自MNIST,一半来自Typeface MNIST。除了"数字"标签,我还引入一个简单的二元标签"is-handwritten",表示是否为手写数字,MNIST的手写数字为1(真),TMNIST的"印刷体"数字为0(假)。我们可以据此修改因果DAG,得到图5.6。

图5.6 展示了结合 MNIST 和 TMNIST 数据的因果DAG,其中"is-handwritten"标签为1表示MNIST图像,为0表示TMNIST图像。

板模型(Plate Modeling)是一种用于概率机器学习的可视化技术,它能很好地展示高层次的抽象,同时保留低层次的维度细节。板符号(Plate notation)是一种视觉表示DAG中重复变量的方法(例如图5.4中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> X 1 X_1 </math>X1 到 <math xmlns="http://www.w3.org/1998/Math/MathML"> X 1 6 X_16 </math>X16),在我们的例子中,像素是重复的变量。

与其将784个像素分别绘制为独立节点,我们使用一个矩形或"板"将重复变量分组为子图。然后在板上标注一个数字,表示该板中实体的重复次数。板可以嵌套,表示重复实体内还有重复实体。每个板用一个字母下标索引该板中的元素。

图5.7中的因果DAG表示了一张图像。

图5.7 展示了因果DAG的板模型表示。板表示重复的变量,在这里是28 × 28 = 784个像素。 <math xmlns="http://www.w3.org/1998/Math/MathML"> X j X_j </math>Xj 表示第 j 个像素。

在训练过程中,我们会有大量的训练图像。接下来,我们将修改该DAG以涵盖训练数据中的所有图像。

5.2 训练神经因果模型

为了训练我们的神经因果模型,我们需要加载并准备训练数据,创建模型架构,编写训练流程,并实现一些用于评估训练进展的工具。我们将从加载和准备数据开始。

5.2.1 设置训练数据

我们的训练数据包含 N 张示例图像,因此需要让板模型表示训练数据中的所有 N 张图像,其中一半为手写,另一半为印刷体。我们将在模型中添加另一个板,表示重复的 N 组图像和标签,如图5.8所示。

现在我们有了一个因果DAG,它既展示了我们期望的因果抽象层次,也包含了训练神经网络所需的维度信息。接下来,我们先加载Pyro和其他一些库,并设置一些超参数。

环境配置

本代码使用Python版本3.10.12编写,并在Google Colab中测试。主要库的版本包括:Pyro(pyro-ppl)1.8.4,torch 2.2.1,torchvision 0.18.0+cu121,pandas 2.0.3。我们还将使用matplotlib进行绘图。

访问 www.altdeep.ai/p/causalaib... 可获取在Google Colab加载笔记本的链接。

如果你的设备支持GPU,使用CUDA(GPU并行计算平台)训练神经网络会更快。下面的代码可以帮我们切换是否使用CUDA。如果你没有GPU或不确定,请将 USE_CUDA 保持为 False。

ini 复制代码
import torch   
USE_CUDA = False   #1
DEVICE_TYPE = torch.device("cuda" if USE_CUDA else "cpu")
#1 如果可用,使用CUDA。

接下来,我们将继承 Dataset 类(用于加载和预处理数据),创建一个新的类来合并MNIST和TMNIST数据集。

python 复制代码
from torch.utils.data import Dataset

import numpy as np
import pandas as pd
from torchvision import transforms

class CombinedDataset(Dataset):    #1
    def __init__(self, csv_file):
        self.dataset = pd.read_csv(csv_file)

    def __len__(self):
        return len(self.dataset)

    def __getitem__(self, idx):
        images = self.dataset.iloc[idx, 3:]     #2
        images = np.array(images, dtype='float32')/255.   #2
        images = images.reshape(28, 28)    #2
        transform = transforms.ToTensor()     #2
        images = transform(images)     #2
        digits = self.dataset.iloc[idx, 2]     #3
        digits = np.array([digits], dtype='int')     #3
        is_handwritten = self.dataset.iloc[idx, 1]     #4
        is_handwritten = np.array([is_handwritten], dtype='float32')    #4
        return images, digits, is_handwritten    #5
#1 此类加载并处理合并后的MNIST和TMNIST数据集,输出torch.utils.data.Dataset对象。
#2 加载、归一化,并重塑图像为28×28像素。
#3 获取并处理数字标签(0-9)。
#4 手写数字(MNIST)标记为1,印刷数字(TMNIST)标记为0。
#5 返回图像、数字标签和手写标签的元组。

然后,我们将用 DataLoader 类(支持高效的数据迭代和批处理)加载GitHub上的CSV数据,并划分为训练集和测试集。

ini 复制代码
from torch.utils.data import DataLoader
from torch.utils.data import random_split

def setup_dataloaders(batch_size=64, use_cuda=USE_CUDA):     #1
    combined_dataset = CombinedDataset(
        "https://raw.githubusercontent.com/altdeep/causalML/master/datasets/combined_mnist_tmnist_data.csv"
    )
    n = len(combined_dataset)     #2
    train_size = int(0.8 * n)    #2
    test_size = n - train_size    #2
    train_dataset, test_dataset = random_split(     #2
        combined_dataset,   #2
        [train_size, test_size],    #2
        generator=torch.Generator().manual_seed(42)   #2
    )     #2
    kwargs = {'num_workers': 1, 'pin_memory': use_cuda} #2
    train_loader = DataLoader(     #3
        train_dataset,     #3
        batch_size=batch_size,     #3
        shuffle=True,     #3
        **kwargs    #3
    )     #3
    test_loader = DataLoader(   #3
        test_dataset,    #3
        batch_size=batch_size,    #3
        shuffle=True,     #3
        **kwargs   #3
    )     #3
    return train_loader, test_loader
#1 设置数据加载器,加载数据并划分为训练集和测试集。
#2 80%数据用于训练,20%用于测试。
#3 创建训练和测试数据加载器。

接下来,我们将搭建完整的变分自编码器。

5.2.2 搭建变分自编码器

变分自编码器(VAE)或许是最简单的深度概率机器学习模型之一。应用于图像的典型设置中,引入一个潜在连续变量 Z,其维度远小于图像数据的维度。这里的维度指数据向量表示中的元素数量。例如,我们的图像是一个28 × 28的像素矩阵,也可以看作一个维度为784的向量。潜变量 Z 通过低维表示压缩了图像信息。数据集中每张图像对应一个潜变量 Z,代表该图像的编码。图5.9对此做了示意说明。

在训练过程中,变量 Z 是潜变量,用虚线表示。(模型部署后,digit 和 is-handwritten 也成为潜变量。)

在因果DAG中,Z 出现为一个新的父节点,但需要注意的是,经典的变分自编码器(VAE)框架并不将 Z 定义为因果变量。现在我们采用因果视角,赋予 Z 因果解释。具体来说,作为图像节点的父节点,我们将 digit 和 is-handwritten 视为影响图像内容的因果驱动因素。但图像中还有其他因素(例如手写字符的笔画粗细,或印刷字符的字体)也是图像表现的原因。我们将 Z 看作是这些未被显式建模的其他图像因果因素的连续潜变量代理,就像 digit 和 is-handwritten 一样。这些因果因素的例子包括 TMNIST 标签中各种字体的细微差别,以及手写数字因不同书写者和书写动作导致的各种变化。基于此,我们可以将条件概率 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( X ∣ digit , is-handwritten , Z ) P(X \mid \text{digit}, \text{is-handwritten}, Z) </math>P(X∣digit,is-handwritten,Z)视为图像 X 的因果马尔可夫核。但重要的是要记住,我们学习到的 Z 表示的是潜在因果因素的代理,而不等同于直接学习实际的潜在因果因素。

VAE结构会训练两个深度神经网络:一个称为"编码器"(encoder),将图像编码为 Z 的数值;另一个称为"解码器"(decoder),与我们的因果DAG对应。解码器根据数字标签、是否手写标签和 Z 值生成图像,如图5.10所示。

解码器类似于渲染引擎;给定 Z 的编码值以及 digit 和 is-handwritten 的值,它渲染出一张图像。

图5.10 解码器神经网络以输入的 Z 以及标签 is-handwritten 和 digit 生成输出图像 X。与任何神经网络一样,输入数据会通过一个或多个"隐藏层"进行处理。

关键的 VAE 概念回顾

  • 变分自编码器(VAE) --- 深度生成建模中广泛使用的框架。我们用它来建模因果模型中的因果马尔可夫核。
  • 解码器(Decoder) --- 我们用解码器作为因果马尔可夫核的模型。它将观察到的因果变量 is-handwritten 和 digit 以及潜变量 Z 映射到图像结果变量 X。

这种 VAE 方法允许我们用神经网络(即解码器)捕捉建模图像为 digit 和 is-handwritten 因果效应时所需的复杂非线性关系。之前讨论的条件概率表和其他简单参数化方法很难对图像进行建模。

首先,实现解码器。我们传入参数 <math xmlns="http://www.w3.org/1998/Math/MathML"> z _ d i m z\_dim </math>z_dim 表示潜变量 Z 的维度, <math xmlns="http://www.w3.org/1998/Math/MathML"> h i d d e n _ d i m hidden\_dim </math>hidden_dim 表示隐藏层的维度(宽度)。实例化完整的 VAE 时会指定这些参数。解码器将潜在向量 Z 与额外输入(数字变量和是否手写的二元指示)结合,输出一个784维向量,代表28 × 28像素的图像。该输出向量为每个像素提供伯努利分布参数,实际上是为每个像素建模"开启"的概率。该类使用两个全连接层(fc1 和 fc2),激活函数采用 Softplus 和 Sigmoid,模拟神经网络中神经元的工作方式。

ini 复制代码
from torch import nn

class Decoder(nn.Module):    #1
    def __init__(self, z_dim, hidden_dim):
        super().__init__()
        img_dim = 28 * 28     #2
        digit_dim = 10    #3
        is_handwritten_dim = 1    #4
        self.softplus = nn.Softplus()     #5
        self.sigmoid = nn.Sigmoid()    #5
        encoding_dim = z_dim + digit_dim + is_handwritten_dim     #6
        self.fc1 = nn.Linear(encoding_dim, hidden_dim)   #6
        self.fc2 = nn.Linear(hidden_dim, img_dim)    #7

    def forward(self, z, digit, is_handwritten):     #8
        input = torch.cat([z, digit, is_handwritten], dim=1) #9
        hidden = self.softplus(self.fc1(input))    #10
        img_param = self.sigmoid(self.fc2(hidden))    #11
        return img_param
#1 VAE中使用的解码器类
#2 图像大小为28×28像素
#3 digit为0-9的独热编码,长度为10的向量
#4 is_handwritten为是否手写的指示,大小为1
#5 Softplus和Sigmoid是非线性激活函数,用于层间映射
#6 fc1为线性函数,将Z向量、digit和is_handwritten映射为线性输出,并经过Softplus激活生成隐藏层向量,长度为hidden_dim
#7 fc2将隐藏层线性映射到输出,并通过Sigmoid激活,输出值在0到1之间
#8 定义从潜变量Z到生成图像X的前向计算
#9 将Z和标签合并
#10 计算隐藏层
#11 线性映射后经过Sigmoid,输出784维参数向量,每个元素对应图像像素的伯努利参数

我们在因果模型中使用该解码器。因果DAG作为因果概率机器学习模型的框架,借助解码器定义了 <math xmlns="http://www.w3.org/1998/Math/MathML"> { i s _ h a n d w r i t t e n , d i g i t , X , Z } \{is\_handwritten, digit, X, Z\} </math>{is_handwritten,digit,X,Z} 的联合概率分布,其中 Z 是潜变量。模型可用于计算给定 Z 值时训练数据的似然。

潜变量 z、表示数字身份的独热向量 digit 和二元指示变量 is_handwritten 都被建模为来自标准分布的样本。然后这些变量输入解码器,生成表示图像每个像素概率的伯努利分布参数(img_param)。

注意,使用伯努利分布来建模像素是一种折中。像素不是二值黑白,而是灰度值。代码行 dist.enable_validation(False) 让我们能够通过伯努利分布的对数似然计算"作弊",以适配图像和解码器输出。

以下模型代码为PyTorch神经网络模块的类方法,完整类稍后展示。

ini 复制代码
import pyro
import pyro.distributions as dist

dist.enable_validation(False)    #1
def model(self, data_size=1):     #2
    pyro.module("decoder", self.decoder)    #2
    options = dict(dtype=torch.float32, device=DEVICE_TYPE)
    z_loc = torch.zeros(data_size, self.z_dim, **options)    #3
    z_scale = torch.ones(data_size, self.z_dim, **options)   #3
    z = pyro.sample("Z", dist.Normal(z_loc, z_scale).to_event(1))    #3
    p_digit = torch.ones(data_size, 10, **options)/10     #4
    digit = pyro.sample(     #4
        "digit",    #4
        dist.OneHotCategorical(p_digit)     #4
    )    #4
    p_is_handwritten = torch.ones(data_size, 1, **options)/2     #5
    is_handwritten = pyro.sample(    #5
        "is_handwritten",     #5
        dist.Bernoulli(p_is_handwritten).to_event(1)     #5
    )     #5
    img_param = self.decoder(z, digit, is_handwritten)   #6
    img = pyro.sample("img", dist.Bernoulli(img_param).to_event(1))   #7
    return img, digit, is_handwritten
#1 关闭分布验证,允许Pyro计算像素的对数似然,尽管像素不是二值
#2 单张图像的模型方法。注册PyTorch模块解码器,告诉Pyro解码器网络中的参数
#3 对Z、digit和is_handwritten建模,分别从标准分布采样。Z采样自多元正态,均值为零,方差为一
#4 digit采样自独热分类分布,数字均等概率
#5 is_handwritten采样自伯努利分布
#6 解码器将digit、is_handwritten和Z映射到概率参数向量
#7 参数向量传入伯努利分布,模拟数据中像素值。虽然像素不是严格的伯努利变量,我们这里放宽该假设

上述模型方法表示一张图像的数据生成过程(DGP)。下一段代码中的 training_model 方法将该模型应用于训练数据中的 N 张图像。

python 复制代码
def training_model(self, img, digit, is_handwritten, batch_size):     #1
    conditioned_on_data = pyro.condition(     #2
        self.model,
        data={
            "digit": digit,
            "is_handwritten": is_handwritten,
            "img": img
        }
    )
    with pyro.plate("data", batch_size):    #3
        img, digit, is_handwritten = conditioned_on_data(batch_size)
    return img, digit, is_handwritten
#1 模型表示一张图像的DGP,training_model应用于训练数据中的N张图像
#2 根据训练数据对模型进行条件化
#3 该上下文管理器表示图5.9中大小为N的板,代表数据中重复的独立同分布样本。此处N为批大小,类似for循环遍历批内数据

我们的概率机器学习模型建模 <math xmlns="http://www.w3.org/1998/Math/MathML"> { Z , X , d i g i t , i s _ h a n d w r i t t e n } \{Z, X, digit, is\_handwritten\} </math>{Z,X,digit,is_handwritten}的联合分布。但因为 Z 是潜变量,模型需要学习 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( Z ∣ X , d i g i t , i s _ h a n d w r i t t e n ) P(Z \mid X, digit, is\_handwritten) </math>P(Z∣X,digit,is_handwritten)。由于我们用解码器神经网络从 Z 和标签映射到 X,条件于 X 和标签的 Z 分布将会很复杂。我们使用变分推断技术,先定义一个近似分布 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q ( Z ∣ X , d i g i t , i s _ h a n d w r i t t e n ) Q(Z \mid X, digit, is\_handwritten) </math>Q(Z∣X,digit,is_handwritten),并努力使其尽可能接近真实分布 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( Z ∣ X , d i g i t , i s _ h a n d w r i t t e n ) P(Z \mid X, digit, is\_handwritten) </math>P(Z∣X,digit,is_handwritten)。

近似分布的核心是VAE框架中的第二个神经网络------编码器,如图5.11所示。编码器将训练数据中观察到的图像及其标签映射到潜变量 Z。

编码器的工作是将图像中的信息压缩成低维的编码表示。

到目前为止的关键 VAE 概念

  • 变分自编码器(VAE) --- 深度生成建模中广泛使用的框架,我们用它来建模因果模型中的因果马尔可夫核。
  • 解码器(Decoder) --- 用于建模因果马尔可夫核,将观察到的因果变量 is-handwritten 和 digit 以及潜变量 ZZZ 映射到图像结果变量 XXX。
  • 编码器(Encoder) --- 将图像、digit 和 is-handwritten 指示映射到潜变量 Z 的分布参数,从该分布中可采样 Z 的值。

在以下代码中,编码器接受图像、数字标签和是否手写的指示作为输入。这些输入被拼接后通过一系列带有 Softplus 激活函数的全连接层。编码器的最终输出由两个向量组成,分别表示潜变量 Z 的位置参数( <math xmlns="http://www.w3.org/1998/Math/MathML"> z l o c z_{loc} </math>zloc)和尺度参数( <math xmlns="http://www.w3.org/1998/Math/MathML"> z s c a l e z_{scale} </math>zscale),条件为观测到的图像(img)、数字标签(digit)和手写指示(is_handwritten)。

ini 复制代码
class Encoder(nn.Module):     #1
    def __init__(self, z_dim, hidden_dim):
        super().__init__()
        img_dim = 28 * 28     #2
        digit_dim = 10  #3
        is_handwritten_dim = 1
        self.softplus = nn.Softplus()    #4
        input_dim = img_dim + digit_dim + is_handwritten_dim     #5
        self.fc1 = nn.Linear(input_dim, hidden_dim)   #5
        self.fc21 = nn.Linear(hidden_dim, z_dim) #6
        self.fc22 = nn.Linear(hidden_dim, z_dim)    #6

    def forward(self, img, digit, is_handwritten):    #7
        input = torch.cat([img, digit, is_handwritten], dim=1)     #8
        hidden = self.softplus(self.fc1(input))    #9
        z_loc = self.fc21(hidden)     #10
        z_scale = torch.exp(self.fc22(hidden))  #10
        return z_loc, z_scale
#1 编码器为PyTorch模块的一个实例
#2 输入图像为28 × 28 = 784像素
#3 数字标签长度为10
#4 编码器只使用Softplus激活函数
#5 线性变换fc1结合Softplus将784维像素向量、10维数字标签向量和1维手写指示向量映射至隐藏层
#6 线性变换fc21和fc22将隐藏层映射至潜变量Z的向量空间
#7 定义从观测变量X映射到潜变量Z的前向过程
#8 合并图像、数字标签和手写指示作为输入
#9 计算隐藏层输出
#10 生成潜变量分布的均值和标准差参数

编码器输出潜变量 Z 的分布参数。训练时,给定图像及其标签(is-handwritten 和 digit),我们希望获得 Z 的良好取值,因此编写了引导函数(guide function),利用编码器对 Z 采样。

ini 复制代码
def training_guide(self, img, digit, is_handwritten, batch_size):     #1
    pyro.module("encoder", self.encoder)    #2
    options = dict(dtype=torch.float32, device=DEVICE_TYPE)
    with pyro.plate("data", batch_size):     #3
        z_loc, z_scale = self.encoder(img, digit, is_handwritten)    #4
        normal_dist = dist.Normal(z_loc, z_scale).to_event(1)    #4
        z = pyro.sample("Z", normal_dist)     #5
#1 training_guide是VAE中调用编码器的方法
#2 注册编码器,使Pyro识别其权重参数
#3 与training_model中的板符号相同,用于迭代批数据
#4 编码器将图像及标签映射为正态分布参数
#5 从该正态分布采样Z

我们将上述元素整合为一个PyTorch神经网络模块,代表完整的VAE。将潜变量维度设置为50,编码器和解码器的隐藏层维度均设置为400。给定图像维度28 × 28,二元的is-handwritten和10维独热编码的digit,输入维度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 28 × 28 + 1 + 10 = 795 28 \times 28 + 1 + 10 = 795 </math>28×28+1+10=795,压缩为400维隐藏层,再压缩为50维的均值和方差参数,用于多元正态分布。解码器输入digit、is-handwritten和Z,映射至400维隐藏层和28 × 28维图像输出。潜变量维度、层数、激活函数和隐藏层大小依赖于具体问题,通常通过经验或试验确定。

ini 复制代码
class VAE(nn.Module):
    def __init__(
        self,
        z_dim=50,    #1
        hidden_dim=400,    #2
        use_cuda=USE_CUDA,
    ):
        super().__init__()
        self.use_cuda = use_cuda
        self.z_dim = z_dim
        self.hidden_dim = hidden_dim
        self.setup_networks()

    def setup_networks(self):     #3
        self.encoder = Encoder(self.z_dim, self.hidden_dim)
        self.decoder = Decoder(self.z_dim, self.hidden_dim)
        if self.use_cuda:
            self.cuda()

    model = model     #4
    training_model = training_model    #4
    training_guide = training_guide    #4
#1 设置潜变量维度为50
#2 隐藏层维度为400
#3 初始化编码器和解码器
#4 添加model、training_model和training_guide方法

完成VAE定义后,我们可以开始训练。

5.2.3 训练流程

我们认为当编码器能将图像编码为潜变量 Z,再通过解码器重构出图像时,就得到了一个好的生成模型。训练过程中,我们可以最小化重构误差------即原始图像与重构图像之间的差异。

关于"变分推断"训练算法的一些说明

在本节中,你会看到许多关于变分推断的术语。理解我们为何使用这种算法很重要。对于神经网络权重和其他参数的拟合以及因果推断,有多种统计估计器和算法可用,其中之一就是变分推断。

需要明确的是,变分推断不是一个"因果"概念,而是一种概率推断算法。本书更倾向使用该算法,因为它即使面对训练数据中潜变量也能很好地扩展,且适用于深度神经网络,能利用PyTorch等深度学习框架。这使得我们能够对文本、图像、视频等丰富模态进行因果推理,而传统因果推断多针对数值数据。此外,该方法可针对不同问题定制(参见第1章"推断商品化"讨论),并能在推断中利用领域知识(例如在引导函数中利用条件独立知识)。变分推断的核心概念也贯穿许多深度生成模型(如潜变量扩散模型)。

实际上,仅最小化重构误差会导致过拟合等问题,因此我们采用概率方法:给定图像,我们用引导函数从 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( Z ∣ i m a g e , i s _ h a n d w r i t t e n , d i g i t ) P(Z|image, is\_handwritten, digit) </math>P(Z∣image,is_handwritten,digit)采样一个 Z 值,再将其传入模型解码器,输出对应 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( i m a g e ∣ i s _ h a n d w r i t t e n , d i g i t , Z ) P(image|is\_handwritten, digit, Z) </math>P(image∣is_handwritten,digit,Z) 的参数。通过这种概率方法最小化重构误差,我们优化编码器和解码器,最大化 Z 关于 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( Z ∣ i m a g e , i s _ h a n d w r i t t e n , d i g i t ) P(Z|image, is\_handwritten, digit) </math>P(Z∣image,is_handwritten,digit) 的似然和原图像关于 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( i m a g e ∣ i s _ h a n d w r i t t e n , d i g i t , Z ) P(image|is\_handwritten, digit, Z) </math>P(image∣is_handwritten,digit,Z) 的似然。

但通常我们无法直接从 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( Z ∣ i m a g e , i s _ h a n d w r i t t e n , d i g i t ) P(Z|image, is\_handwritten, digit) </math>P(Z∣image,is_handwritten,digit) 采样或计算似然,因此引导函数尝试对其进行近似。引导函数代表一个变分分布,记作 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q ( Z ∣ X , i s _ h a n d w r i t t e n , d i g i t ) Q(Z|X, is\_handwritten, digit) </math>Q(Z∣X,is_handwritten,digit)。编码器权重的变化即是对变分分布的调整。训练时,我们优化编码器权重,使变分分布尽可能接近真实后验分布 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( Z ∣ i m a g e , i s _ h a n d w r i t t e n , d i g i t ) P(Z|image, is\_handwritten, digit) </math>P(Z∣image,is_handwritten,digit)。该训练方法称为变分推断,其核心是最小化两分布间的KL散度,即衡量两分布差异的指标。

变分推断过程优化一个称为 ELBO(对数似然的期望下界)的目标函数。最小化负ELBO损失间接实现了重构误差和变分分布与后验分布间KL散度的最小化。Pyro通过工具 Trace_ELBO 实现了ELBO。

我们采用随机变分推断(SVI),即在训练时使用数据的随机子集("批次")而非全部数据,减少内存消耗并提高扩展性。

到目前为止的关键 VAE 概念

  • 变分自编码器(VAE) --- 深度生成建模中广泛使用的框架,用于建模因果马尔可夫核。
  • 解码器(Decoder) --- 将观察的因果变量 is-handwritten 和 digit 以及潜变量 ZZZ 映射到图像结果变量 XXX。
  • 编码器(Encoder) --- 将图像、digit 和 is-handwritten 映射到潜变量 ZZZ 的分布参数。
  • 引导函数(Guide function) --- 训练时生成 Z 值的函数,采样自变分分布 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q ( Z ∣ i m a g e , i s _ h a n d w r i t t e n , d i g i t ) Q(Z|image, is\_handwritten, digit) </math>Q(Z∣image,is_handwritten,digit),近似后验分布。
  • 变分分布(Variational distribution) --- 引导函数代表的分布,用于在推断中采样。
  • 变分推断(Variational inference) --- 优化变分分布 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q ( Z ∣ . . . ) Q(Z∣...) </math>Q(Z∣...)以逼近真实后验 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( Z ∣ . . . ) P(Z∣...) </math>P(Z∣...),通过最小化KL散度。
  • 随机变分推断(SVI) --- 使用随机小批量数据进行变分推断,提升训练速度和扩展性。

辅助函数:绘制图像

ini 复制代码
def plot_image(img, title=None):     #1
    fig = plt.figure()
    plt.imshow(img.cpu(), cmap='Greys_r', interpolation='nearest')
    if title is not None:
        plt.title(title)
    plt.show()
#1 用于绘制图像的辅助函数

辅助函数:重构和比较图像

ini 复制代码
import matplotlib.pyplot as plt

def reconstruct_img(vae, img, digit, is_hw, use_cuda=USE_CUDA):     #1
    img = img.reshape(-1, 28 * 28)
    digit = F.one_hot(torch.tensor(digit), 10)
    is_hw = torch.tensor(is_hw).unsqueeze(0)
    if use_cuda:
        img = img.cuda()
        digit = digit.cuda()
        is_hw = is_hw.cuda()
    z_loc, z_scale = vae.encoder(img, digit, is_hw)
    z = dist.Normal(z_loc, z_scale).sample()
    img_expectation = vae.decoder(z, digit, is_hw)
    return img_expectation.squeeze().view(28, 28).detach()

def compare_images(img1, img2):    #2
    fig = plt.figure()
    ax0 = fig.add_subplot(121)
    plt.imshow(img1.cpu(), cmap='Greys_r', interpolation='nearest')
    plt.axis('off')
    plt.title('original')
    ax1 = fig.add_subplot(122)
    plt.imshow(img2.cpu(), cmap='Greys_r', interpolation='nearest')
    plt.axis('off')
    plt.title('reconstruction')
    plt.show()
#1 该函数通过编码器编码图像,再通过解码器重构图像
#2 并将原图与重构图并排显示,便于视觉比较

辅助函数:处理数据

ini 复制代码
import torch.nn.functional as F

def get_random_example(loader):    #1
    random_idx = np.random.randint(0, len(loader.dataset))    #1
    img, digit, is_handwritten = loader.dataset[random_idx]     #1
    return img.squeeze(), digit, is_handwritten    #1

def reshape_data(img, digit, is_handwritten):     #2
    digit = F.one_hot(digit, 10).squeeze()     #2
    img = img.reshape(-1, 28*28)     #2
    return img, digit, is_handwritten     #2

def generate_coded_data(vae, use_cuda=USE_CUDA):     #3
    z_loc = torch.zeros(1, vae.z_dim)     #3
    z_scale = torch.ones(1, vae.z_dim)     #3
    z = dist.Normal(z_loc, z_scale).to_event(1).sample()     #3
    p_digit = torch.ones(1, 10)/10     #3
    digit = dist.OneHotCategorical(p_digit).sample()     #3
    p_is_handwritten = torch.ones(1, 1)/2     #3
    is_handwritten = dist.Bernoulli(p_is_handwritten).sample()    #3
    if use_cuda:     #3
        z = z.cuda() #3
        digit = digit.cuda() #3
        is_handwritten = is_handwritten.cuda()     #3
    img = vae.decoder(z, digit, is_handwritten)     #3
    return img, digit, is_handwritten    #3

def generate_data(vae, use_cuda=USE_CUDA):     #4
    img, digit, is_handwritten = generate_coded_data(vae, use_cuda)    #4
    img = img.squeeze().view(28, 28).detach()    #4
    digit = torch.argmax(digit, 1)   #4
    is_handwritten = torch.argmax(is_handwritten, 1)     #4
    return img, digit, is_handwritten     #4
#1 从数据集中随机选取样本
#2 重塑数据格式
#3 生成编码后的数据
#4 生成未编码的数据

训练过程设置

ini 复制代码
from pyro.infer import SVI, Trace_ELBO
from pyro.optim import Adam

pyro.clear_param_store()     #1
vae = VAE()    #2
train_loader, test_loader = setup_dataloaders(batch_size=256)    #3
svi_adam = Adam({"lr": 1.0e-3})     #4
model = vae.training_model    #5
guide = vae.training_guide    #5
svi = SVI(model, guide, svi_adam, loss=Trace_ELBO())     #5
#1 清空引导函数参数存储
#2 初始化VAE模型
#3 加载数据
#4 初始化Adam优化器
#5 初始化SVI损失计算器,损失函数为负ELBO

训练生成模型时,建立一个用测试数据评估训练进展的流程非常有用。你可以监控任何有助于训练的指标。这里我计算并打印测试损失,确保测试损失随训练损失逐步降低(如果训练损失持续降低但测试损失停滞,则可能过拟合)。

更直观的方式是生成并查看图像。在测试评估中,我展示两幅图像:首先重构随机测试图像,将其经过编码器和解码器后,和原图并排显示;其次,直接从模型生成新图像并展示。此代码会每隔一定训练周期运行一次。

测试评估函数

ini 复制代码
def test_epoch(vae, test_loader):
    epoch_loss_test = 0     #1
    for img, digit, is_hw in test_loader:    #1
        batch_size = img.shape[0]    #1
        if USE_CUDA:    #1
            img = img.cuda()    #1
            digit = digit.cuda()     #1
            is_hw = is_hw.cuda()  #1
        img, digit, is_hw = reshape_data(     #1
            img, digit, is_hw     #1
        )   #1
        epoch_loss_test += svi.evaluate_loss(     #1
            img, digit, is_hw, batch_size    #1
        )    #1
    test_size = len(test_loader.dataset)    #1
    avg_loss = epoch_loss_test/test_size     #1
    print("Epoch: {} avg. test loss: {}".format(epoch, avg_loss))     #1
    print("Comparing a random test image to its reconstruction:")    #2
    random_example = get_random_example(test_loader)     #2
    img_r, digit_r, is_hw_r = random_example    #2
    img_recon = reconstruct_img(vae, img_r, digit_r, is_hw_r)     #2
    compare_images(img_r, img_recon)     #2
    print("Generate a random image from the model:")    #3
    img_gen, digit_gen, is_hw_gen = generate_data(vae)    #3
    plot_image(img_gen, "Generated Image")     #3
    print("Intended digit: ", int(digit_gen))    #3
    print("Intended as handwritten: ", bool(is_hw_gen == 1))     #3
#1 计算并打印测试损失
#2 比较随机测试图像及其重构图像
#3 从模型生成随机图像并展示

运行训练并绘制训练进展

ini 复制代码
NUM_EPOCHS = 2500
TEST_FREQUENCY = 10

train_loss = []
train_size = len(train_loader.dataset)

for epoch in range(0, NUM_EPOCHS+1):   #1
    loss = 0
    for img, digit, is_handwritten in train_loader:
        batch_size = img.shape[0]
        if USE_CUDA:
            img = img.cuda()
            digit = digit.cuda()
            is_handwritten = is_handwritten.cuda()
        img, digit, is_handwritten = reshape_data(
            img, digit, is_handwritten
        )
        loss += svi.step(    #2
            img, digit, is_handwritten, batch_size     #2
        )     #2
    avg_loss = loss / train_size
    print("Epoch: {} avgs training loss: {}".format(epoch, loss))
    train_loss.append(avg_loss)
    if epoch % TEST_FREQUENCY == 0:    #3
        test_epoch(vae, test_loader)    #3
#1 训练指定次数的epoch
#2 对单个批次进行训练步
#3 每隔10个epoch运行测试评估

完整代码及Jupyter笔记本链接见 www.altdeep.ai/p/causalaib...,可在Google Colab运行。

5.2.4 训练评估

训练过程中,我们会随机选择一张图像,先通过编码器编码为潜变量 ZZZ,再通过解码器将其重构。某次运行时,我看到第一张测试图像是一张非手写数字6。图5.12展示了该图像及其重构。

训练中,我们还会从生成模型中模拟随机图像并绘制。图5.13展示了某次运行中的首张模拟图像,这次是数字3。

但模型学习速度很快。经过130个训练周期后,我们得到了图5.14所示的结果。

训练完成后,可以在图5.15中看到训练过程中的损失(负ELBO)的可视化。

代码会训练编码器的参数,将图像和标签映射到潜变量。同时也会训练解码器,将潜变量和标签映射回图像。潜变量是VAE的核心特征,但我们需要更深入地探讨如何从因果角度解释这个潜变量。

5.2.5 我们应该如何从因果角度理解 ZZZ?

我之前提到,可以将 ZZZ 看作图像中对象所有独立潜在原因的"代理"。ZZZ 是我们从图像像素中学习到的一个表示。虽然很容易将该表示视为这些潜在原因的更高级别因果抽象,但它很可能并没有很好地实现因果抽象的效果。自编码器范式训练一个编码器,将图像嵌入到低维表示 ZZZ 中,目标是以尽可能小的损失重构原始图像。为了尽量减少重构损失,模型会尽可能多地编码原始图像的信息到低维空间。

然而,一个好的因果表示不应该试图捕捉尽可能多的信息,而应专注于捕获图像中的因果信息,并忽略其他无关内容。事实上,当 ZZZ 是无监督的(即没有关于 ZZZ 的标签)时,要实现"因果因素与非因果因素"的解耦通常是不可能的。但领域知识、干预和半监督方法能够提供帮助。更多关于因果表示学习和因果因素解耦的参考,请见 www.altdeep.ai/p/causalaib... 。随着本书的深入,我们将逐步建立对这种表示中"因果信息"应有形态的直觉。

5.2.6 这种因果解释的优势

我们的VAE的结构和训练过程本身并没有内在的因果性质;它和许多机器学习场景中常见的普通监督式VAE类似。我们这里唯一的因果成分是我们的解释:我们认为digit和is-handwritten是原因,Z 是潜在原因的代理,图像是结果。根据因果马尔可夫性质,我们的因果模型将联合分布分解为 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( Z ) 、 P ( is-handwritten ) 、 P ( digit ) 和 P ( image ∣ Z , is-handwritten , digit ) P(Z)、P(\text{is-handwritten})、P(\text{digit}) 和 P(\text{image}|Z, \text{is-handwritten}, \text{digit}) </math>P(Z)、P(is-handwritten)、P(digit)和P(image∣Z,is-handwritten,digit),后者即图像的因果马尔可夫核。

那么,我们能用这种因果解释做什么?首先,我们可以利用它来改进深度学习和通用机器学习的流程与任务。下一节中,我们会通过半监督学习的例子来展示这一点。

5.3 利用因果推断增强深度学习

我们可以利用因果洞察来改进深度学习模型的搭建和训练方式。这些洞察通常带来诸如提升样本效率(即用更少的数据做更多事)、实现迁移学习(用一个任务学到的知识提升另一个任务表现)、数据融合(合并不同数据集)以及增强模型预测的鲁棒性等好处。

深度学习很多时候是试错的过程。比如训练VAE或其他深度模型时,通常需要尝试不同的框架(VAE与其他模型)、架构选择(潜变量维度、隐藏层大小、激活函数、层数等)和训练策略(损失函数、学习率、优化器等),才能获得较好效果。这些尝试耗费大量时间和资源。在某些情况下,因果建模能帮助我们更明智地选择哪些方法可能有效,哪些可能无效,从而节约成本。本节将通过半监督学习的一个案例来探讨这种应用。

5.3.1 机制独立性作为归纳偏置

假设我们有一个因果DAG,包含两个变量:"原因" C 和"结果" O,结构简单为 <math xmlns="http://www.w3.org/1998/Math/MathML"> C → O C \to O </math>C→O。因果马尔可夫核为 P(C) 和 P(O|C)。回顾第3章的"机制独立性"思想------因果马尔可夫核 P(O|C) 表示"原因 C 如何驱动结果 O"的机制,这个机制与系统中其他机制是独立的,其他机制的变化不会影响 P(O|C)。因此,了解 P(O∣C) 无法告诉你关于原因分布 P(C) 的信息,反之亦然。但知道结果分布 P(O) 可能帮助推断条件分布 P(C∣O),反之亦然。

举例来说,设 C 代表防晒霜使用情况,O 代表是否晒伤。你知道防晒霜如何防晒(紫外线、SPF、防晒频率、出汗游泳影响等机制),即 P(O|C)。但这并不告诉你防晒霜的使用普及率 P(C)。

如果你想推测晒伤的人是否使用了防晒霜(即推测 P(C∣O)),此时知道晒伤的普遍程度 P(O) 就有帮助。若晒伤常见,那么即使晒伤了,使用防晒霜的人也可能更多;若晒伤罕见,人们预防意识可能较低。

类似地,设 C是学习努力,O 是考试成绩。你知道学习多如何导致高分(机制 P(O∣C)),但不知有多少学生努力学习(P(C))。当你试图推断某学生是否努力学习时(P(C∣O)),知道成绩分布 P(O) 有助于判断。例如低分罕见时,学生可能更容易放松,不努力学习。这个认识可以作为一种归纳偏置,限制你对 P(C∣O) 的建模。

因果归纳偏置

"归纳偏置"指的是推断算法在多种可能推断中偏好某些推断的假设(显性或隐性)。比如奥卡姆剃刀原理,或预测未来趋势会延续过去趋势的假设。

现代深度学习通过网络结构和训练目标来编码归纳偏置。例如,卷积神经网络中的"卷积"和"最大池化"实现了"平移不变性"的归纳偏置------即一只小猫无论出现在图像左侧还是右侧,模型都能识别它。

因果模型通过对数据生成过程(DGP)的因果假设(比如因果DAG)提供归纳偏置。深度学习可以利用这些因果归纳偏置,提升效果,就像利用其他归纳偏置一样。比如,机制独立性表明,知道 P(O) 可以作为学习 P(C|O)的有用归纳偏置。

考虑两个变量 X 和 Y(可以是向量),其联合分布为 P(X, Y)。我们设计算法学习解决某任务,数据来自 P(X, Y)。概率链式法则告诉我们:

<math xmlns="http://www.w3.org/1998/Math/MathML"> P ( X = x , Y = y ) = P ( X = x ∣ Y = y ) P ( Y = y ) = P ( Y = y ∣ X = x ) P ( X = x ) P(X=x,Y=y)=P(X=x∣Y=y)P(Y=y)=P(Y=y∣X=x)P(X=x) </math>P(X=x,Y=y)=P(X=x∣Y=y)P(Y=y)=P(Y=y∣X=x)P(X=x)

从概率角度看,建模集合 <math xmlns="http://www.w3.org/1998/Math/MathML"> { P ( X ∣ Y ) , P ( Y ) } \{P(X|Y), P(Y)\} </math>{P(X∣Y),P(Y)} 等价于建模 <math xmlns="http://www.w3.org/1998/Math/MathML"> { P ( Y ∣ X ) , P ( X ) } \{P(Y|X), P(X)\} </math>{P(Y∣X),P(X)}。

但如果 X 是 Y 的原因,或者 Y 是 X 的原因,机制独立性在集合 <math xmlns="http://www.w3.org/1998/Math/MathML"> { P ( X ∣ Y ) , P ( Y ) } \{P(X|Y), P(Y)\} </math>{P(X∣Y),P(Y)} 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> { P ( Y ∣ X ) , P ( X ) } \{P(Y|X), P(X)\} </math>{P(Y∣X),P(X)}之间造成非对称性(特别是 <math xmlns="http://www.w3.org/1998/Math/MathML"> { P ( Y ∣ X ) , P ( X ) } \{P(Y|X), P(X)\} </math>{P(Y∣X),P(X)} 代表了 X 对 Y 的因果影响机制,而 <math xmlns="http://www.w3.org/1998/Math/MathML"> { P ( X ∣ Y ) , P ( Y ) } \{P(X|Y), P(Y)\} </math>{P(X∣Y),P(Y)} 不具备这一点)。这种非对称性可以作为归纳偏置引导算法学习。半监督学习就是很好的例子。

5.3.2 案例研究:半监督学习

回到我们基于VAE的TMNIST-MNIST因果模型,假设除了原始数据外,我们还有大量未标注的数字图像(即没有观察到digit和is-handwritten标签)。根据我们对模型的因果解释,这部分数据可以在训练中利用半监督学习进行训练。

机制独立性可以帮助判断半监督学习何时有效。在监督学习中,训练数据由 N 个 <math xmlns="http://www.w3.org/1998/Math/MathML"> X , Y X, Y </math>X,Y对组成: <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 1 , y 1 ) , ( x 2 , y 2 ) , ... , ( x N , y N ) (x_1, y_1), (x_2, y_2), \dots, (x_N, y_N) </math>(x1,y1),(x2,y2),...,(xN,yN)。X 是用于预测标签 Y 的特征数据。之所以称为"监督",是因为每个 x 都有对应的 y。我们用这些对来学习条件概率分布 P(Y∣X)。

而在无监督学习中,只有特征数据 X 是已知的,没有标签 Y 的观测值,数据形如 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x 1 ) , ( x 2 ) , ... , ( x N ) (x_1), (x_2), \dots, (x_N) </math>(x1),(x2),...,(xN)。仅凭这类数据,我们无法直接学习 P(Y∣X),只能学习 P(X)。

半监督学习的问题是:假如我们同时拥有监督数据和无监督数据,这两种数据能否结合起来,使得预测 Y 的能力优于仅用监督数据的情况?换句话说,是否能通过无监督数据对 P(X) 的学习来辅助对 P(Y|X) 的学习?

半监督学习问题非常实际。当标注数据代价高昂时,无监督数据往往很丰富。比如,在社交媒体平台上,假设你需要构建一个算法来判定上传图片是否包含无故暴力内容。首先要创建监督数据,让人工对图片进行暴力与否的标注。这不仅耗费大量人力,还给标注员带来心理压力。一个成功的半监督方法可以极大减少标注量。

我们的任务是学习联合分布 P(X, Y) 的表示,并用其来预测 P(Y∣X)。为了让半监督学习有效,未标注的 X 必须能更新联合分布 P(X, Y) 的表示,从而对 P(Y|X)提供信息。

然而,机制独立性意味着学习 P(X,Y) 这项任务会分解成学习各个因果马尔可夫核的不同表示,每个表示的参数向量相互正交(详见第3.2节)。这种参数模块化会阻止未标注的 X 的观察结果对 P(Y∣X) 的表示进行更新。

为说明这一点,我们考虑两种情况:一种是 Y 是 X 的原因,另一种是 X 是 Y 的原因。

  • 若 Y 是 X 的原因,如我们的MNIST-TMNIST例子中(Y 是is-handwritten和digit变量,X 是图像),学习任务分解成学习 P(X∣Y) 和 P(Y) 的不同表示。未标注的 X 可以帮助我们更好地表示 P(X),进而通过贝叶斯规则将 P(X∣Y) 转化为 P(Y∣X)。
  • 若 X 是 Y 的原因,学习任务分解成学习 P(X)和 P(Y∣X)的不同表示。参数模块化意味着未标注的 XXX 只能帮助我们更新 P(X)的表示,而不能更新 P(Y∣X) 的表示。

因此,半监督学习的有效性在很大程度上依赖于变量的因果方向性。

特征导致标签的情况有时被称为因果学习,因为预测的方向是从因到果。而标签导致特征的情况称为反因果学习。两种情况见图5.16所示。

机制独立性表明,半监督学习只有在反因果学习的情况下(相对于仅用有标签数据进行监督学习的基线)才能取得性能提升。更多详细解释和参考文献见章节注释 www.altdeep.ai/causalAIboo...。但直观上,这与防晒霜与晒伤的例子类似------知道晒伤的普遍性P(O)有助于推断某人是否使用了防晒霜,即 P(C∣O)。在这种反因果学习情况下,仅从 P(X)的观测数据出发,仍能有助于学习 P(Y∣X) 的良好模型;但在因果学习情况下,这将是资源和精力的浪费。

实际上,X 和 Y 之间的因果结构可能比简单的 <math xmlns="http://www.w3.org/1998/Math/MathML"> X → Y X \to Y </math>X→Y 或 <math xmlns="http://www.w3.org/1998/Math/MathML"> X ← Y X \leftarrow Y </math>X←Y 更加复杂和微妙,例如可能存在未观测的共同原因。这里的结论是:当你对机器学习问题中变量的因果关系有一定了解时,即使任务不是因果推断(例如仅仅是预测 Y 给定 X),也可以利用这些知识更有效地建模。这有助于避免在不太可能成功的方法上浪费时间和资源,比如半监督学习的情况,或者能够实现更高效、更稳健或性能更优的推断。

5.3.3 用因果方法解开深度学习之谜

我们前面的半监督学习示例强调了因果视角如何解释半监督学习何时有效、何时失败。换句话说,它在一定程度上揭开了半监督学习的神秘面纱。

深度学习方法效果背后的神秘感,促使AI研究者Ali Rahimi将现代机器学习比作炼金术。

炼金术确实有用:炼金术士发明了冶金、染料制造、现代玻璃制造工艺和药物。与此同时,他们也相信可以用水蛭治病,将贱金属变成黄金。

换言之,炼金术奏效,但炼金术士并不了解其背后的科学原理,因此难以判断何时失败。结果,炼金术士在死胡同(如"贤者之石"、长生不老药)上浪费了大量精力。

章节总结

  • 将深度学习纳入因果模型:

    • 计算机视觉问题的因果模型
    • 训练深度因果图像模型
  • 使用因果推理提升机器学习:

    • 机制独立性与半监督学习案例研究
    • 用因果方法解密深度学习

同样,深度学习"有效",体现在它在多种预测和推断任务上取得良好表现,但我们常常不清楚它为什么以及何时有效。这种神秘导致了可重复性、稳健性和安全性问题,也带来了不负责任的AI应用,比如试图从个人资料照片预测行为(如犯罪倾向)的研究,这类工作犹如含有汞等有毒物质的炼金术长生不老药,不仅无效还会带来危害。

我们常听说深度学习的"超人"表现。说到超人,想象一下另一种超人起源故事:如果他首次公开亮相时,超能力不稳定呢?他能飞行、超强力和激光视线,但飞行时不时失败,力量时强时弱,激光视线有时失控,造成严重的附带伤害。公众既惊叹又期待,但面对高风险情况时又不敢轻易依赖。

再想象他的养父母是因果推断专家,他们用因果分析模型化了他的超能力的"来龙去脉",解开了超能力背后的机制之谜,进而研发出一种稳定超能力的药丸。这种药丸不会赋予他新能力,只会让已有能力更可靠。研发过程可能没有飞行和激光那样轰动,但它是区分"拥有超能力"和"真正成为超人"的关键。

这个比喻帮助我们理解利用因果方法解开深度学习和其他机器学习方法神秘感的重要性。减少神秘感有助于开发更稳健的方法,并避免浪费或有害的应用。

总结

深度学习可以用来增强因果建模和推断,因果推理能够提升深度学习模型的设计、训练及性能表现。

因果模型可以利用深度学习处理高维非线性关系和良好扩展性的能力。

你可以使用生成式AI框架,如变分自编码器(VAE),基于DAG构建因果生成模型,就像我们用pgmpy做的那样。

解码器将直接父节点的结果(图像的标签)映射到子节点的结果(图像本身)。

换句话说,解码器为图像的因果Markov核提供了一个非线性、高维的表示。

编码器则将图像变量及其原因(标签)映射回潜变量Z。

我们可以将潜变量的学习表示视为未建模因果的替代表示,但它仍然缺乏理想因果表示应具备的特质。潜在因果表示的学习是当前的研究热点。

因果性通常能增强深度学习及其他机器学习方法,帮助揭示其背后的基本原理。例如,因果分析表明,在反因果学习(特征由标签导致)的情况下,半监督学习是有效的;而在因果学习(特征导致标签)情况下则不然。

这些因果洞见能帮助建模者避免在不适合的问题场景下,浪费时间、计算资源和人力资源去尝试可能无效的算法。

因果洞见还能解开深度学习模型构建与训练的神秘面纱,使模型更加稳健、高效和安全。

相关推荐
大尾巴青年7 分钟前
06 一分钟搞懂langchain的Agent是如何工作的
langchain·llm
小阿鑫12 分钟前
记录第一次公司内部分享:如何基于大模型搭建企业+AI业务
大模型·llm·agent·大模型落地·ai落地·mcp·mcpserver
weixin_4786897632 分钟前
【conda配置深度学习环境】
人工智能·深度学习·conda
我想睡觉26133 分钟前
Python训练营打卡DAY44
开发语言·人工智能·python·深度学习·算法·机器学习
硬核隔壁老王36 分钟前
从零开始搭建RAG系统系列(三):数据准备与预处理
人工智能·程序员·llm
硬核隔壁老王40 分钟前
从零开始搭建RAG系统系列(四):⽂档向量化与索引构建
人工智能·程序员·llm
攻城狮7号2 小时前
AI浪潮下的思辨:傅盛访谈之我见
人工智能·深度学习·agent
Francek Chen2 小时前
【深度学习优化算法】02:凸性
人工智能·pytorch·深度学习·优化算法·凸函数
寻丶幽风2 小时前
论文阅读笔记——Large Language Models Are Zero-Shot Fuzzers
论文阅读·pytorch·笔记·深度学习·网络安全·语言模型
要努力啊啊啊3 小时前
GQA(Grouped Query Attention):分组注意力机制的原理与实践《一》
论文阅读·人工智能·深度学习·语言模型·自然语言处理