当前位置:AIGC资讯 > AIGC > 正文

【大模型理论篇】生成式模型算法原理深入浅出(涉及Stable Diffusion、生成对抗网络、高斯混合模型、隐马尔可夫模型、朴素贝叶斯等算法原理分析及生成式模型解释)

1. 背景介绍

         随着大模型的推出,“生成式AI”这个名词一夜之间席卷大江南北。甚至很多人的概念里,“生成式AI”等同于人工智能。但事实上,人工智能(Artificial Intelligence)涵盖的范围要广的多,生成式AI只是其中的一个部分,一个分支,它能够让机器从大量数据集中学习模式,然后根据这些模式自主生成新内容,比如生成文本、图像、视频和音频等,都有很多的案例。

        人工智能主流的两种算法模式为“判别式模型”和“生成式模型”。这两种模型各自有不同的应用场景:

 判别式模型(Discriminative Models)

一句话概括:判别式模型区分不同类型的数据实例。

目标:学习输入数据与标签之间的边界。即给定一个输入,判别式模型直接学习如何将其分类到一个类别中。

示例:逻辑回归、支持向量机(SVM)、条件随机场(CRF)、深度神经网络(如卷积神经网络 CNN 和循环神经网络 RNN)。

优点:通常在分类任务中表现出色,因为它们直接建模条件概率 P(y|x)。

应用:图像分类、文本分类、语音识别等。

生成式模型(Generative Models)

一句话概括:生成式模型可以生成新的数据实例。

目标:学习数据的分布,以生成新的样本。即给定一个样本,生成式模型尝试建模输入数据的总体分布P(x) 或联合分布P(x, y)。

示例:朴素贝叶斯、高斯混合模型(GMM)、隐马尔可夫模型(HMM)、生成对抗网络(GAN)、变分自编码器(VAE),扩散模型(Stable Diffusion)。

优点:能够生成新的样本,通常用于图像生成、文本生成等任务。

应用:图像生成、文本生成、数据增强等。当然像朴素贝叶斯也能用于分类。

进一步对比分析:

生成模型旨在捕捉数据集中类别的实际分布。 判别模型建模数据集类别的决策边界。

与判别模型相比,生成模型在计算上更为昂贵。

生成模型对无监督机器学习任务非常有用。判别模型对有监督的机器学习任务非常有用。

生成模型比判别模型更容易受到异常值的影响。

为什么说生成模型比判别模型更容易受到异常值的影响?

生成模型:生成模型的目标是学习数据的整体分布,包括所有类别的联合分布 p(x, y)。为了实现这一目标,生成模型需要对每个类别的数据分布进行建模。这意味着它们必须对整个数据空间进行建模,包括异常值。异常值对数据分布的估计有直接影响,因为生成模型在训练过程中试图准确地表示数据的真实分布,依赖于对每个类别的整个数据分布的建模,异常值可能会对模型参数(如均值、协方差矩阵等)产生显著影响,例如,高斯混合模型(GMM)中的异常值可以显著影响高斯分布的均值和协方差。因此异常值可能导致模型对这些异常点进行过度拟合,从而影响总体模型性能。

判别模型:判别模型的目标是学习类别之间的边界,即类别条件概率 p(y|x)或直接学习决策函数。这些模型主要关注如何区分不同的类别,而不是建模数据的整体分布。判别模型的训练过程更侧重于分类决策边界,而不是整个数据分布的建模。例如,支持向量机(SVM)在构建分类边界时,主要关注支持向量而非所有训练样本。因此,异常值的影响相对较小,因为它们不直接影响决策边界的建立。

2. 大模型(LLMs)与生成式AI的关系

        那么大模型与生成式人工智能之间是什么关系?

一句话解释:生成式人工智能建立在基础人工智能模型之上。大模型(LLM)是生成式人工智能中生成文本的部分。

        进一步地,大模型(LLM)和学习联合概率分布 p(x,y)之间存在密切关系,LLMs、生成式人工智能、学习联合概率分布 p(x,y) 在概念上是相关的:

生成式模型:生成式模型是指可以对数据进行建模并生成新数据的模型。GPT系列模型能够用于学习文本数据的分布,从而生成类似于训练数据的新文本。

学习联合概率分布 p(x,y):学习联合概率分布 p(x,y) 意味着学习两个随机变量 x 和 y 的联合分布。GPT模型可以被视为学习联合概率分布的一种方法,试图建模文本观测数据 的联合分布,从而能够生成符合该分布的新文本。

应用:通过学习 p(x,y),GPT模型不仅可以用于生成新数据,还可以用于语言建模、文本生成、机器翻译等任务。LLMs模型能够从训练数据中学习到数据的分布,然后利用这种学习生成新的数据样本。

3.生成式模型算法原理介绍

        有了上述知识背景,我们接下来会选择一些比较有代表性的生成式模型,来具体描述生成式模型的算法原理,帮助建立更全面的认知体系。生成式机器学习算法很多,本文选择了5种算法,包括简单的模型(NB)、聚类算法(GMM)、传统的自然语言建模算法(HMM、LDA)、基于深度学习的生成对抗网络模型(GAN)以及近段时间火热的文本生成图像(text-to-image)算法(Stable Diffusion)。

3.1 朴素贝叶斯(Naive Bayesian)

        朴素贝叶斯一般用于分类,基于贝叶斯定理,模型假设预测变量之间相互独立。

原理

贝叶斯定理: 贝叶斯定理描述了在已知某些条件下,某个事件发生的概率。其公式为:


其中:
        P(C∣X)是在特征 X 给定的情况下,类别 C 的后验概率。
        P(X∣C)是在类别 C给定的情况下,特征 X 的似然概率。
        P(C)是类别 C 的先验概率。
        P(X)是特征 X 的边际概率。

条件独立性假设: 朴素贝叶斯假设给定类别的情况下,所有特征都是条件独立的。因此:

                                
这使得计算 P(X∣C)时,只需要计算每个特征的概率并将它们相乘。

每个特征的条件概率 可以通过训练数据中在类别下特征的频率来估算:

计算边际概率 P(X)(通常是常数), 边际概率 P(X)是所有类别下 X 的概率总和。在分类中,一般是可以被省略的,因为它在所有类别中是相同的:

在实际分类时,通常不直接计算这个值,而是可以通过比较 来选择具有最大后验概率的类别。

分类过程:

 在分类过程中,模型计算每个类别的后验概率,然后选择具有最大后验概率的类别作为预测结果:
                                                    

由于分母 P(X)在所有类别中是相同的,所以可以简化为:

                                       
即:

朴素贝叶斯优缺点

优点:

简单易懂:模型的实现和解释都比较简单。 计算效率高:即使在特征数量非常大的情况下,也能迅速进行分类。 对小数据集有效:在数据量较小的情况下,依然能够获得比较好的结果。

缺点:

特征独立性假设:假设特征之间独立,在实际应用中往往不成立,可能导致准确性下降。 处理相关性差:对特征之间存在的相关性处理不佳,可能影响分类效果。

不过朴素贝叶斯在许多应用中仍然非常有效,尤其是当特征间的相关性较弱时。

         原理介绍完毕,可以看到朴素贝叶斯分类器,通过对输入的数据学习得到每个类别的分布情况,当要对某个样本进行分类时,根据样本的特征和所有类别的联合分布P(X, C),得到该样本处于每个分布的位置,即该分布产生这个样本的概率,认为最大概率对应的类别为该样本的类别,完成分类。

3.2 高斯混合模型(GMM)

        高斯混合模型(Gaussian Mixture Model, GMM)是一种生成模型,用于对数据进行聚类和建模。GMM假设数据是由多个高斯分布组成的,每个高斯分布代表一个簇或子群体。模型的目标是确定每个高斯分布的参数,并根据这些分布来对数据进行聚类或者生成。

3.2.1  模型定义

        高斯混合模型假设数据 X 是由 K 个不同的高斯分布混合而成的。每个高斯分布具有不同的均值和协方差矩阵。模型定义如下:

数据生成过程:

选择一个高斯分布 k(从 K 个分布中选择),根据其混合权重。 从所选的高斯分布中生成一个数据点。

高斯分布: 每个高斯分布的概率密度函数为:

其中:

 是第k个高斯分布的均值向量。 ​ 是第k个高斯分布的协方差矩阵。 d 是数据的维度。

混合模型: 整个数据的概率密度函数是所有高斯分布加权和:

其中:

​ 是第k个高斯分布的混合权重,满足和

3.2.2 模型参数估计

        模型的目标是估计每个高斯分布的均值 ​、协方差矩阵 ​ 和混合权重 。这通常通过期望最大化(Expectation-Maximization, EM)算法来完成:

期望步骤(E步骤): 计算每个数据点属于每个高斯分布的概率(即“责任度”):

 

其中 ​ 表示数据点属于第 k 个高斯分布的概率。

最大化步骤(M步骤): 更新模型参数以最大化对数似然函数:

      其中 N是数据点的总数。

这里给出一个代码示例,帮助更好地理解EM过程,计算过程还是比较简单的,稍后给出理论分析:

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm, gaussian_kde
import seaborn as sns

# Generate a dataset with two Gaussian components
mu1, sigma1 = 2, 1
mu2, sigma2 = -1, 0.8
X1 = np.random.normal(mu1, sigma1, size=200)
X2 = np.random.normal(mu2, sigma2, size=600)
X = np.concatenate([X1, X2])
 
# Plot the density estimation using seaborn
sns.kdeplot(X)
plt.xlabel('X')
plt.ylabel('Density')
plt.title('Density Estimation of X')
plt.show()

# Initialize parameters
mu1_hat, sigma1_hat = np.mean(X1), np.std(X1)
mu2_hat, sigma2_hat = np.mean(X2), np.std(X2)
pi1_hat, pi2_hat = len(X1) / len(X), len(X2) / len(X)

# Perform EM algorithm for 20 epochs
num_epochs = 20
log_likelihoods = []
 
for epoch in range(num_epochs):
    # E-step: Compute responsibilities
    gamma1 = pi1_hat * norm.pdf(X, mu1_hat, sigma1_hat)
    gamma2 = pi2_hat * norm.pdf(X, mu2_hat, sigma2_hat)
    total = gamma1 + gamma2
    gamma1 /= total
    gamma2 /= total
     
    # M-step: Update parameters
    mu1_hat = np.sum(gamma1 * X) / np.sum(gamma1)
    mu2_hat = np.sum(gamma2 * X) / np.sum(gamma2)
    sigma1_hat = np.sqrt(np.sum(gamma1 * (X - mu1_hat)**2) / np.sum(gamma1))
    sigma2_hat = np.sqrt(np.sum(gamma2 * (X - mu2_hat)**2) / np.sum(gamma2))
    pi1_hat = np.mean(gamma1)
    pi2_hat = np.mean(gamma2)
     
    # Compute log-likelihood
    log_likelihood = np.sum(np.log(pi1_hat * norm.pdf(X, mu1_hat, sigma1_hat)
                                   + pi2_hat * norm.pdf(X, mu2_hat, sigma2_hat)))
    log_likelihoods.append(log_likelihood)
    
# Plot log-likelihood values over epochs
plt.plot(range(1, num_epochs+1), log_likelihoods)
plt.xlabel('Epoch')
plt.ylabel('Log-Likelihood')
plt.title('Log-Likelihood vs. Epoch')
plt.show()

# Plot the final estimated density
X_sorted = np.sort(X)
density_estimation = pi1_hat*norm.pdf(X_sorted, mu1_hat, sigma1_hat) + pi2_hat * norm.pdf(X_sorted, mu2_hat, sigma2_hat)
 
plt.plot(X_sorted, gaussian_kde(X_sorted)(X_sorted), color='green', linewidth=2)
plt.plot(X_sorted, density_estimation, color='red', linewidth=2)
plt.xlabel('X')
plt.ylabel('Density')
plt.title('Density Estimation of X')
plt.legend(['Kernel Density Estimation','Mixture Density'])
plt.show()

        一旦确定了混合高斯分布的参数,就可以用来做聚类等任务,使用贝叶斯定理和估计的模型参数,可以估计后验分量分配概率。知道一个数据点更可能来自一个分量分布而不是另一个分量分布,提供了一种学习聚类的方法,其中聚类分配由最可能的分量分配确定。

3.2.3 期望最大化算法(Expectation-Maximization Algorithm)

        在第二部分涉及的EM算法,需要展开讲一下,帮助理解EM算法面向解决的问题、怎么做等内容。在实际的机器学习应用中,往往会有许多相关特征,但其中只有一部分是可观察的。在处理那些有时可观察到但有时又不可观察的变量时,可以利用变量在可见或可观察时的实例来学习和预测那些不可观察的实例。这种方法通常被称为处理缺失数据。通过使用可观察变量的可用实例,机器学习算法可以从观察到的数据中学习模式和关系。这些学习到的模式可以被用于预测在缺失或不可观察的情况下变量的值。

        期望最大化(EM)算法可以用于处理变量部分可观察的情况。当某些变量是可观察时,可以利用这些实例来学习和估计它们的值。然后可以预测这些变量在不可观察实例中的值。EM算法适用于潜变量(latent variable),即那些不可直接观察但可以从其他观察到的变量的值推断出的变量。通过利用这些潜变量的概率分布的已知一般形式,EM算法可以预测它们的值。事实上,EM算法可以被用于无监督聚类算法,它提供了一个框架,用于找到统计模型的局部最大似然参数,并在数据缺失或不完整的情况下推断潜变量。比如常见的KMeans算法就是EM算法的一个特殊变种,其假设聚类是球形的。

        以下是期望最大化(EM)算法中一些常用的术语:

潜变量(Latent Variables):潜变量是统计模型中未观察到的变量,无法直接测量,但可以通过其对可观察变量的影响间接推断出来。

似然(Likelihood):是给定模型参数的情况下观察到给定数据的概率。在EM算法中,目标是找到最大化似然的参数。

对数似然(Log-Likelihood):是似然函数的对数,衡量观察数据与模型之间的拟合优度。EM算法的目标是最大化对数似然。

最大似然估计(Maximum Likelihood Estimation, MLE):MLE是一种通过寻找最大化似然函数的参数值来估计统计模型参数的方法,似然函数衡量模型解释观察数据的程度。

后验概率(Posterior Probability):在贝叶斯推断的背景下,EM算法可以扩展为估计最大后验(MAP)估计,其中基于先验分布和似然函数计算参数的后验概率。

期望(E)步骤(Expectation (E) Step):EM算法的E步骤计算给定观察数据和当前参数估计的潜变量的期望值或后验概率。它涉及计算每个数据点的潜变量的概率。

最大化(M)步骤(Maximization (M) Step):EM算法的M步骤通过最大化从E步骤获得的期望对数似然来更新参数估计。它涉及通过数值优化方法找到优化似然函数的参数值。

收敛(Convergence):收敛指的是EM算法已达到稳定解的状态。通常通过检查对数似然或参数估计的变化是否低于预定的阈值来确定。

        期望最大化(EM)算法是一种迭代优化方法,由估计步骤(E步骤)和最大化步骤(M步骤)组成,形成一个迭代过程,提高模型拟合度。通过迭代重复这些步骤,EM 算法旨在最大化观察数据的似然性。

计算步骤:

初始化(Initialization):

最初,考虑一组参数的初始值。系统接收一组不完整的观察数据,并假设这些观察数据来自特定的模型。

E步骤(期望步骤, E-Step):

在这个步骤中,我们利用观察数据来估计或推测缺失或不完整数据的值。基本上,这一步用于更新变量。

计算每个潜变量的后验概率或责任值,基于观察数据和当前的参数估计。 使用当前的参数估计来估计缺失或不完整的数据值。 基于当前的参数估计和估计的缺失数据,计算观察数据的对数似然。

M步骤(最大化步骤, M-Step):

在这个步骤中,利用前面“期望”步骤中生成的完整数据来更新参数的值。基本上,这一步用于更新假设。

通过最大化从E步骤获得的期望完整数据对数似然来更新模型参数。 涉及解决优化问题,以找到最大化对数似然的参数值。

收敛(Convergence):

在这个步骤中,检查参数值是否收敛。如果收敛,则停止;否则,重复步骤2和步骤3,即“期望”步骤和“最大化”步骤,直到收敛发生。

通过比较迭代之间对数似然或参数值的变化来检查收敛。 如果变化低于预定的阈值,则停止并认为算法已收敛。 否则,回到E步骤,重复过程直到收敛实现。

为什么EM一定能收敛到我们想要的结果?另外参数的更新公式如何而来?本文通过更一般的数学形式来证明。

假设有观测数据 X 和隐含变量 Z,模型参数为。目标是最大化对数似然函数:

由于隐含变量 Z 是不可观测的,直接最大化是困难的。EM算法通过引入隐含变量的期望来解决这个问题。

考虑包含隐含变量 Z的完整数据的对数似然函数:

可以将其拆分为对数联合概率:

进一步地:

在E步中,计算给定当前参数 下隐含变量的后验分布的期望。我们定义期望对数似然函数 :

这里的期望是对 Z在给定当前参数  下的后验分布进行的:

代入对数联合概率的公式:

由于 对 Z 的求和是常数项(与无关),我们可以分开处理:

关于该公式中的第二部分:

这个部分是关于隐含变量 Z 的对数概率。具体来说:

是在给定参数 下隐含变量 Z 的概率分布。由于在当前的期望计算中,这个概率分布已经被确定下来。所以在M步优化时,这部分不会影响优化结果。 是在给定当前参数下隐含变量 Z 的后验概率。这部分仅用于计算对数似然的期望值,而在M步中这部分不会直接影响参数 的优化。

因此,在M步中,只需最大化与参数相关的第一部分:

这部分表示在当前隐含变量后验分布下,观测数据的对数似然函数的期望值。它是关于的函数,可以通过优化得到新的参数估计。

在M步中,最大化来更新参数 。即:

这意味着你已经在当前的期望对数似然函数下找到了一组新的参数 ,使得对数似然函数的期望值最大化。

由于 是对数似然函数 的下界(根据Jensen不等式),每次迭代EM算法都会使对数似然函数不降低:

这一性质确保了EM算法的收敛性,即每次迭代对数似然函数的值不会降低,从而逐步逼近最优解。

对于高斯混合模型(GMM),需要处理每个高斯分布的对数似然函数

展开对数似然函数:

对于每个参数 ,例如高斯混合模型中的均值 、协方差矩阵  和混合权重​,期望对数似然函数可以展开为具体的函数形式。对于 GMM,更新公式涉及到加权样本的均值和协方差矩阵:

均值 ​:

协方差矩阵:

混合权重 :

求解优化问题:

对于每个模型参数,通过求解优化问题来找到使期望对数似然函数最大化的参数值。一般需要对 关于参数进行求导,并解得梯度为零的点。

       这里给出GMM的参数更新公式求导证明:

对 关于参数  求导的具体过程如下:

一般形式

假设  是参数的向量,,我们需要对每个进行求导:

高斯混合模型

混合权重 :

对于混合权重,期望对数似然函数部分是:

对​求导:

为了找到最大值,需要满足:

解得:

均值 :

对均值 的期望对数似然函数部分是:

对 求导:

设导数为零:

协方差矩阵 :

对协方差矩阵 的期望对数似然函数部分是:

对求导:

设导数为零:

总结:

       在高斯混合模型(Gaussian Mixture Model, GMM)中,联合概率分布体现了数据点 x 和其对应类别标签 y 的联合概率分布 p(x, y)。GMM 通过混合多个高斯分布来建模这种联合分布。p(x∣y=k) 是数据点 x 在给定类别 k 下的条件概率分布(高斯分布)。p(y) 是类别k的先验概率(混合权重。整个模型的边际概率分布p(x)是通过所有类别的高斯分布的加权和来表示的。

3.3 隐马尔可夫模型(HMM)

        马尔可夫链可以被看作是带有概率的图,这些概率表示从链中的一个点(“状态”)移动到另一个状态的可能性。隐马尔可夫模型(HMM)使用一个不可见的、无法直接观察到的马尔可夫链。数据输入到模型中后,当前状态和紧接前一个状态的概率被用来计算最可能的结果。

3.3.1 HMM 的组成

一个隐马尔可夫模型通常包括以下三个主要成分:

隐状态集合:

隐马尔可夫模型有一个有限的隐状态集合。 隐状态是模型内部的状态,不直接观察到的,但影响观察到的数据。

观测符号集合:

观测符号集合  是可以直接观察到的数据。 每个隐状态生成观测符号的概率分布。

模型参数:

状态转移概率:从一个隐状态转移到另一个隐状态的概率,记为 ,其中 。 观测概率:在给定隐状态的情况下,生成某个观测符号的概率,记为 ,其中 。 初始状态概率:初始时刻各隐状态的概率,记为 ,其中 。        

3.3.2 HMM 模型的主要目标

1. 参数估计:给定观测数据,估计HMM的参数,包括状态转移概率、观测概率和初始状态概率。

        在给定观测序列和初始HMM参数的情况下,优化的目标是调整这些参数,使得模型能够更好地解释观测数据。这个过程涉及使用 Baum-Welch算法,这是 EM(Expectation-Maximization)算法 的一种应用,关于EM算法原理在GMM中已做分析,可以看一下。

其中:

:初始状态概率 :状态转移概率 :观测概率
Baum-Welch 算法

        目标:最大化观测数据的对数似然函数。

对数似然函数:

其中,q是隐状态序列,是观测序列和隐状态序列的联合概率。

                                

E 步骤:计算给定当前参数的情况下,隐状态的后验概率。

计算 :

                   

计算:

                   

M 步骤:更新模型参数以最大化期望对数似然函数。

更新状态转移概率:

 

更新观测概率:

更新初始状态概率:

2. 解码:根据观测数据找出最可能的隐状态序列。

        目标:找到给定观测序列 O 下最可能的隐状态序列。这个过程通常使用维特比算法。

维特比算法

        目标:找到使得观测序列的概率最大化的隐状态序列。

维特比路径概率:

                   

递归步骤:

                  

终止步骤:

                 

路径回溯:

使用辅助表 记录每个时间步的最优前驱状态。 从终止状态开始回溯,得到最可能的隐状态序列。

3.3.3 HMM算法细节详解

        隐马尔可夫模型(HMM)的核心算法主要包括前向算法、后向算法、维特比算法和Baum-Welch算法。下面详细介绍这些算法的原理和公式。

1. 前向算法(Forward Algorithm)

目标:计算给定观测序列 的概率 ,其中  是模型参数(包括转移概率、观测概率和初始状态概率)。

公式:

前向概率定义:

其中, 表示在时间步 t 状态为 的情况下,观测序列前 t 个观测的概率。

初始步骤:

其中,是状态​ 的初始概率,是在状态下观测到 的概率。

递归步骤:

其中,是从状态转移到状态  的概率, 是在状态 下观测到 的概率。

终止步骤:

2. 后向算法(Backward Algorithm)

目标:与前向算法类似,计算给定观测序列的概率,但通过后向变量进行。

公式:

后向概率定义:

其中, 表示从时间步 t 到终止时间步 T 的观测序列的概率,给定当前状态为 。

终止步骤:

递归步骤:

前向-后向算法的结合:

3. 维特比算法(Viterbi Algorithm)

目标:寻找给定观测序列 下最可能的隐状态序列。

公式:

维特比概率定义:

其中,表示在时间步 t 状态为  的情况下,前 t 个观测序列的最大概率。

初始步骤:

递归步骤:

路径回溯:

使用一个额外的表 来记录在时间步 t 状态 的最佳前驱状态。 最终,通过回溯  表来找到最可能的隐状态序列。

4. Baum-Welch 算法(EM 算法)

目标:根据给定的观测序列 估计HMM的参数。

公式:

期望步骤(E步骤):

计算每个状态在时间 t 的期望值: 计算每个状态在时间 t 的概率:

最大化步骤(M步骤),参数通过对目标函数求导获得【2】:

更新状态转移概率: 更新观测概率: 更新初始状态概率:

因此:

前向算法 和 后向算法 用于计算观测序列的概率。 维特比算法 用于寻找最可能的状态序列。 Baum-Welch算法 用于估计HMM的参数。

总结:     

        在序列标注问题中,观测序列通常是输入数据的实际内容,而状态序列是标签或标记。对于句子“我 喜欢 学习”,观测序列是 [我, 喜欢, 学习]。状态序列是句子中每个词的词性标签。例如,“我/PRP 喜欢/VB 学习/VB”中,状态序列是 [PRP, VB, VB]。在隐马尔可夫模型中,联合概率 是观测序列和隐状态序列同时发生的概率,用于描述模型在给定状态序列和观测序列时的概率。模型的训练过程通过最大化对数似然函数来调整模型参数。

        【3】给出一个hmm python示例,帮助更好理解hmm如何执行。文件访问可能会有点问题,建议直接下载后执行。示例使用 hmmlearn 分析历史黄金价格,数据来源于:Gold Spot Prices & Market History | World Gold Council。将所需的库以及数据导入 Python,并绘制历史数据图表。计算黄金价格的每日变化,并限制数据从2008年开始(以便包括雷曼危机和 COVID-19!)。通常,处理价格变化而不是实际价格本身,可以更好地建模实际市场状况。示例不是直接建模黄金价格,而是建模黄金价格的每日变化——这使得能够更好地捕捉市场状态。将黄金价格的每日变化拟合到一个包含3个隐藏状态的高斯发射模型中。使用3个隐藏状态的原因是预计每日变化至少会有3种不同的状态——低波动性、中波动性和高波动性。执行后发现模型确实返回了3个独特的隐藏状态。这些数字没有任何内在意义——哪个状态对应于哪种波动性状态必须通过查看模型参数来确认。3个隐藏状态的转移矩阵显示对角线元素相对于非对角线元素较大。这意味着模型倾向于保持在当前状态中——转移到其他状态的概率不高。每个观测值是从多变量高斯分布中抽取的。对于状态0,高斯均值为0.28;对于状态1,均值为0.22;对于状态2,均值为0.27。由于数据是一维的,协方差矩阵简化为每个状态的标量值。对于状态0,协方差为33.9;对于状态1,协方差为142.6;对于状态2,协方差为518.7。这对3种波动性状态的初步假设一致——低波动性时协方差应较小,而高波动性时协方差应非常大。将模型的状态预测与数据绘制在一起,发现状态0、1和2似乎对应于低波动性、中波动性和高波动性。从下面图表中,可以发现高波动性时期对应于经济困难时期,如2008年至2009年的雷曼危机、2011–2012年的经济衰退以及2020年因 COVID-19 导致的衰退。此外,看到在不确定时期,黄金价格往往会上涨,因为投资者增加对黄金的购买,黄金被视为稳定和安全的资产。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from hmmlearn import hmm

# base_dir = "https://github.com/natsunoyuki/Data_Science/blob/master/gold/gold/gold_price_usd.csv?raw=True"
base_dir = "gold_price_usd.csv"

data = pd.read_csv(base_dir)

# Convert the datetime from str to datetime object.
data["datetime"] = pd.to_datetime(data["datetime"])

# Determine the daily change in gold price.
data["gold_price_change"] = data["gold_price_usd"].diff()
print("data['gold_price_change']:")
print(data["gold_price_change"])

# Restrict the data to later than 2008 Jan 01.
data = data[data["datetime"] >= pd.to_datetime("2008-01-01")]

# Plot the daily gold prices as well as the daily change.
plt.figure(figsize = (15, 10))
plt.subplot(2,1,1)
plt.plot(data["datetime"], data["gold_price_usd"])
plt.xlabel("datetime")
plt.ylabel("gold price (usd)")
plt.grid(True)
plt.subplot(2,1,2)
plt.plot(data["datetime"], data["gold_price_change"])
plt.xlabel("datetime")
plt.ylabel("gold price change (usd)")
plt.grid(True)
plt.show()

# Use the daily change in gold price as the observed measurements X.
X = data[["gold_price_change"]].values
# Build the HMM model and fit to the gold price change data.
model = hmm.GaussianHMM(n_components = 3, covariance_type = "diag", n_iter = 50, random_state = 42)
model.fit(X)
# Predict the hidden states corresponding to observed X.
Z = model.predict(X)
states = pd.unique(Z)

print("Unique states:")
print(states)

print("\nStart probabilities:")
print(model.startprob_)

print("\nTransition matrix:")
print(model.transmat_)

print("\nGaussian distribution means:")
print(model.means_)

print("\nGaussian distribution covariances:")
print(model.covars_)

plt.figure(figsize = (15, 10))
plt.subplot(2,1,1)
for i in states:
    want = (Z == i)
    x = data["datetime"].iloc[want]
    y = data["gold_price_usd"].iloc[want]
    plt.plot(x, y, '.')
plt.legend(states, fontsize=16)
plt.grid(True)
plt.xlabel("datetime", fontsize=16)
plt.ylabel("gold price (usd)", fontsize=16)
plt.subplot(2,1,2)
for i in states:
    want = (Z == i)
    x = data["datetime"].iloc[want]
    y = data["gold_price_change"].iloc[want]
    plt.plot(x, y, '.')
plt.legend(states, fontsize=16)
plt.grid(True)
plt.xlabel("datetime", fontsize=16)
plt.ylabel("gold price change (usd)", fontsize=16)
plt.show()

3.4 文档主题模型(LDA)

        潜在狄利克雷分配(Latent Dirichlet Allocation,LDA) 是一种生成模型,用于从文档中自动发现潜在的主题。假设文档是由多个主题生成的,而每个主题是由一组词汇组成的。每篇文档由各种词汇组成,每个主题可以与一些词汇相关联。LDA 的目标是基于文档中包含的词汇来找出文档所属的主题。它假设具有相似主题的文档将使用相似的词汇组。这使得文档能够映射到潜在主题上的概率分布,而主题本身也是概率分布。

3.4.1 LDA 模型的生成过程

        【4】给出了LDA 的图形模型表示。

LDA 的生成过程可以用以下步骤描述:

选择主题分布:

对每个文档 d,从一个狄利克雷分布 中抽取一个主题分布: 其中, 是主题分布的狄利克雷先验参数。

文档主题分布:分布表示了文档中各个主题的概率。例如,​ 是文档 d 中主题 k 的概率。

为每个词选择主题:

对文档 d 中的每个词 : 从主题分布 中抽取一个主题: 从主题 的词汇分布 中抽取一个词 :

选择主题:对于文档中的每个词,首先需要为该词选择一个主题。这个选择是根据文档的主题分布  来进行的。具体地说,对于文档 d 中的第 n 个词 ​,从多项式分布中抽取一个主题 。这个主题 是在所有 K 个主题中随机选择的,概率由 决定。

选择词汇分布:

从一个狄利克雷分布中抽取每个主题的词汇分布:其中, 是词汇分布的狄利克雷先验参数。

主题词汇分布:每个主题有一个词汇分布,这个分布是从狄利克雷分布中抽取的。这个分布表示了在给定主题下,各个词汇的概率。例如,是在主题 k 下词汇 v 的概率。

生成词汇:一旦为词选择了主题,接下来需要从该主题的词汇分布中生成具体的词汇 。这意味着词汇 的选择是根据主题的词汇分布 来决定.

3.4.2 LDA 的学习目标

        在 LDA 中,目标是通过最大化给定文档集合的对数似然函数来学习这些模型参数。可以通过变分推断或吉布斯采样等方法来实现,具体目标如下:

最大化对数似然函数:在给定文档集合的情况下,优化模型参数(主题分布和词汇分布),使得模型生成文档的联合概率分布最大化。

其中:

W 是观测的文档词汇  和  是狄利克雷分布的超参数  是文档的主题分布  是主题的词汇分布 z 是每个词的主题分配

3.4.3 模型参数估计方法

        在LDA模型中,后验分布是不可解析的,一般需要使用 变分推断 或 吉布斯采样 来估计模型的参数:以下是每一项的详细计算公式和方法。

3.4.3.1 变分推断(Variational Inference)

        变分推断是一种通过优化变分下界来近似真实后验分布的方法。其基本思想是引入一个变分分布 q 来近似真实的后验分布 p,并通过最大化变分下界(ELBO)来优化模型参数。

变分分布

        在 LDA 模型中,我们用变分分布来近似后验分布。假设变分分布是各因素之间相互独立的,即:

其中:

是文档主题分布的变分分布 是主题词汇分布的变分分布 是隐状态序列的变分分布
变分下界(ELBO)

变分下界(ELBO)是对数似然函数的一个下界,表示为:

其中:

     表示对变分分布 q 的期望

关于如何推导ELBO,这里给出证明:

1. 定义对数边际似然度

对于给定的数据 W,对数边际似然度是:

其中,Z 表示潜在变量(例如,主题分配)。由于直接计算这个积分非常困难,我们采用变分推断方法来求解。

2. 引入变分分布

引入一个变分分布 q(Z) 来近似后验分布 p(Z∣W)。变分分布 q(Z) 是一个简单的分布,通常选择容易计算的形式。我们的目标是优化 q(Z),使其尽可能接近真实的后验分布。

3. 使用变分分布构建下界

根据对数边际似然度的定义,有:

可以引入变分分布 q(Z) 来重新表示这一积分:

根据对数的性质,有:

应用 Jensen's inequality:

右侧的表达式被称为证据下界(ELBO):

4. 计算 ELBO

ELBO 可以展开为:

其中:

第一项 是期望对数联合概率。 第二项 是变分分布的熵,用来衡量变分分布的复杂度。

进一步将联合概率分解为边际概率和条件概率:

所以 ELBO 可以重新表示为:

5. 目标函数

通过最大化 ELBO,可以得到对数边际似然度的一个上界:

通过优化变分分布 q(Z)的参数,使 ELBO 尽可能接近,从而得到对模型的近似推断。

变分推断步骤
初始化变分分布 、 和 。 优化 ELBO 以更新变分分布的参数。 重复 迭代直到收敛。
3.4.3.2 吉布斯采样(Gibbs Sampling)

        吉布斯采样是一种马尔可夫链蒙特卡罗(MCMC)方法,通过在隐变量的条件分布下进行采样来估计后验分布, 从后验分布中生成样本。对于LDA模型,吉布斯采样需要更新每个词的主题标签 (在文档 d 中第 n 个词的主题)以及主题分布 和主题的词汇分布。吉布斯采样的步骤如下:

采样步骤

初始化 隐状态 ​。

对于每个词 和每个文档 d

从的条件分布【5】中进行采样,其中前部分表示主题的概率、后部分表示给定主题后词的采样概率。: ,​ 其中:  是文档 d 中除了当前词 外的主题 k 的计数 是主题 k 的总计数(除了当前文档) 是主题 k 下词 的计数 是主题 k 的总词计数  和  是狄利克雷分布的超参数 K 是主题的数量 V 是词汇表的大小

更新模型参数 根据采样结果,计算各个主题的分布和词汇分布。

重复 采样步骤直到收敛,需要丢弃初始的样本以获得平稳状态下的样本。

       通过变分推断或吉布斯采样来估计 LDA 模型的参数,进而从文档数据中发现潜在的主题结构。

     【6】给出了针对语料nips论文的代码示例,帮助理解。安装相关的依赖包可能会费点时间,还有注意包版本的兼容性问题。数据文件可以从kaggle网站上下载。参数上设置了10个topic。

# Importing modules
import pandas as pd
import os
# Read data into papers
# https://www.kaggle.com/datasets/benhamner/nips-papers?resource=download
papers = pd.read_csv('papers.csv')
# Print head
papers.head()

# Remove the columns
papers = papers.drop(columns=['id', 'event_type', 'pdf_name'], axis=1).sample(100)
# Print out the first rows of papers
papers.head()

# Load the regular expression library
import re
# Remove punctuation
papers['paper_text_processed'] = \
papers['paper_text'].map(lambda x: re.sub('[,\.!?]', '', x))
# Convert the titles to lowercase
papers['paper_text_processed'] = \
papers['paper_text_processed'].map(lambda x: x.lower())
# Print out the first rows of papers
papers['paper_text_processed'].head()


import gensim
from gensim.utils import simple_preprocess
import nltk
nltk.download('stopwords')
from nltk.corpus import stopwords
stop_words = stopwords.words('english')
stop_words.extend(['from', 'subject', 're', 'edu', 'use'])
def sent_to_words(sentences):
    for sentence in sentences:
        # deacc=True removes punctuations
        yield(gensim.utils.simple_preprocess(str(sentence), deacc=True))
def remove_stopwords(texts):
    return [[word for word in simple_preprocess(str(doc)) 
             if word not in stop_words] for doc in texts]
data = papers.paper_text_processed.values.tolist()
data_words = list(sent_to_words(data))
# remove stop words
data_words = remove_stopwords(data_words)
print(data_words[:1][0][:30])

import gensim.corpora as corpora
# Create Dictionary
id2word = corpora.Dictionary(data_words)
# Create Corpus
texts = data_words
# Term Document Frequency
corpus = [id2word.doc2bow(text) for text in texts]
# View
print(corpus[:1][0][:30])

from pprint import pprint
# number of topics
num_topics = 10
# Build LDA model
lda_model = gensim.models.LdaMulticore(corpus=corpus,
                                       id2word=id2word,
                                       num_topics=num_topics)
# Print the Keyword in the 10 topics
pprint(lda_model.print_topics())
doc_lda = lda_model[corpus]


import pyLDAvis.gensim
import pickle 
import pyLDAvis
# Visualize the topics
pyLDAvis.enable_notebook()
LDAvis_data_filepath = os.path.join('ldavis_prepared_'+str(num_topics))
# # this is a bit time consuming - make the if statement True
# # if you want to execute visualization prep yourself
if 1 == 1:
    LDAvis_prepared = pyLDAvis.gensim.prepare(lda_model, corpus, id2word)
    with open(LDAvis_data_filepath, 'wb') as f:
        pickle.dump(LDAvis_prepared, f)
# load the pre-prepared pyLDAvis data from disk
with open(LDAvis_data_filepath, 'rb') as f:
    LDAvis_prepared = pickle.load(f)
pyLDAvis.save_html(LDAvis_prepared, 'ldavis_prepared_'+ str(num_topics) +'.html')
LDAvis_prepared

3.5 生成对抗网络(Generative Adversarial Network)

        生成对抗网络(Generative Adversarial Network,GAN)【7】是一种生成模型,由两个神经网络组成:生成器(Generator)和判别器(Discriminator)。GAN通常利用卷积神经网络等架构。生成建模的目标是自主识别输入数据中的模式,使模型能够生成新的样本,这些样本在可行的范围内类似于原始数据集。

        生成器生成候选样本,而判别器对其进行评估。竞争是在数据分布的基础上进行的。通常,生成网络学习从潜在空间映射到感兴趣的数据分布,而判别网络则区分由生成器产生的候选样本与真实的数据分布。生成网络的训练目标是增加判别网络的错误率(即通过生成新的候选样本来“欺骗”判别网络,使其认为这些样本不是合成的,而是实际数据分布的一部分)。

        GAN的核心思想是基于通过判别器的“间接”训练,判别器是另一个可以判断输入“真实性”的神经网络,该网络本身也在动态更新。这也就意味着生成器并不是被训练去最小化与特定图像的距离,而是去欺骗判别器。使得模型可以以无监督的方式进行学习。

3.5.1 GAN的架构

生成对抗网络(GANs)可以分为三个部分:

生成(Generative):学习一个生成模型,该模型描述数据如何以概率模型的形式生成。 对抗(Adversarial):对抗意味着将一种事物与另一种事物对立起来。在GAN的上下文中,生成的结果与数据集中的实际图像进行比较。使用判别器的机制来应用一个模型,试图区分真实和虚假的图像。 网络(Networks):使用深度神经网络作为人工智能(AI)算法进行训练。

生成对抗网络(GAN)由两个主要模块组成:生成器和判别器【8、9】。

生成器模型

        生成器模型是生成对抗网络(GAN)中负责创建新且准确数据的关键元素。生成器以随机噪声作为输入,将其转换为复杂的数据样本,例如文本或图像。生成器通常为一个深度神经网络。通过训练,生成器的设计通过可学习的参数层捕捉训练数据的潜在分布。生成器通过反向传播调整其输出,以生成与真实数据相似的样本,从而不断优化其参数。

判别器模型

        生成对抗网络(GANs)中的判别器模型是一个人工神经网络,用于区分生成数据和真实数据。通过评估输入样本并分配真实性概率,判别器充当二分类器。随着时间的推移,判别器学会区分数据集中真实的数据和生成器创建的人工样本。这使得判别器可以逐渐优化其参数,提高识别能力。在处理图像数据时,判别器的架构通常使用卷积层或其他适当的结构。对抗训练的目标是最大化判别器准确识别生成样本为虚假样本和真实样本为真实样本的能力。生成器和判别器的互动使得判别器变得越来越挑剔,这帮助GAN生成非常逼真的合成数据。

3.5.2 GAN的损失函数设置【10】

生成器损失

        GAN中生成器的目标是生成足够真实的合成样本,以欺骗判别器。生成器通过最小化其损失函数 来实现这一点。损失最小化时,表示生成器生成的样本被判别器分类为真实的概率最大。公式如下:

其中:

​ 衡量生成器欺骗判别器的效果。 表示判别器对生成样本的正确分类的对数概率。 生成器的目标是最小化这个损失,从而鼓励生成被判别器分类为真实的样本(接近1)。

判别器损失

        判别器通过减少对生成样本和真实样本正确分类的负对数似然来进行优化。这个损失函数激励判别器准确地将生成的样本标记为虚假样本,将真实样本标记为真实。公式如下:

其中:

评估判别器区分生成样本和实际样本的能力。  表示判别器准确分类真实数据的对数似然。 表示判别器将生成样本标记为虚假的对数机会。 判别器的目标是减少这个损失,通过准确识别人工样本和真实样本。

最小最大损失

在生成对抗网络(GAN)中,最小最大损失公式为:

其中:

G 是生成器网络,D 是判别器网络。 实际数据样本从真实数据分布 中获得,表示为 x。 从先验分布(通常是正态分布或均匀分布)中采样的随机噪声表示为 z。 D(x) 代表判别器正确识别实际数据为真实数据的概率。 D(G(z))代表判别器将生成器生成的数据识别为真实的概率。

怎么理解这个最小最大化损失?

生成器的目标(最小化损失):

生成器希望生成的样本尽可能地让判别器认为是真实的,即使得 D(G(z)) 接近 1。 这使得生成器的损失函数是 ,生成器希望通过最小化这个损失来提高生成样本的质量,使其更难被判别器识别为假数据。

判别器的目标(最大化损失):

判别器希望能够准确地区分真实数据和生成的假数据。 这使得判别器的损失函数是 和 ,判别器通过最大化这两个损失来提升区分真实数据和假数据的能力。

3.5.3 GAN如何运作?

GAN的工作步骤如下:

GAN的工作步骤如下:

初始化:创建两个神经网络:生成器(Generator, G)和判别器(Discriminator, D)。

G 的任务是生成新的数据,比如图像或文本,这些数据尽可能地与真实数据相似。 D 充当评论员,试图区分真实数据(来自训练数据集)和 G 生成的数据。

生成器的第一步:G 接收一个随机噪声向量作为输入。这个噪声向量包含随机值,并作为 G 创建过程的起点。通过其内部层和学习到的模式,G 将噪声向量转换成一个新的数据样本,比如生成的图像。

判别器的回合:D 接收两种输入:

来自训练数据集的真实数据样本。 在前一步中 G 生成的数据样本。D 的任务是分析每个输入,并确定它是真实数据还是 G 生成的伪造数据。它输出一个介于0和1之间的概率分数。分数为1表示数据很可能是真实的,0则表明数据是伪造的。

学习过程:此时,对抗部分开始发挥作用:

如果 D 正确地将真实数据识别为真实(分数接近1),并将生成的数据识别为虚假(分数接近0),那么 G 和 D 都会得到小的奖励。这是因为它们都很好地完成了各自的任务。 然而,关键在于持续改进。如果 D 一直正确地识别所有数据,它的学习效果就会有限。因此,目标是让 G 最终能欺骗 D。

生成器的改进:

当 D 错误地将 G 的生成物标记为真实(分数接近1),在这种情况下,G 会收到显著的正向更新,而 D 则因被欺骗而受到惩罚。 这种反馈帮助 G 改进其生成过程,以生成更真实的数据。

判别器的适应:

相反,如果 D 正确地识别了 G 的伪造数据(分数接近0),但 G 没有得到奖励,则 D 的辨别能力会进一步增强。 G 和 D 之间的持续对抗使得两个网络随着时间的推移不断精进。

        随着训练的进展,G 变得越来越擅长生成真实的数据,使得 D 更难区分真实数据和伪造数据。理想情况下,G 变得如此精通,以至于 D 无法可靠地区分真实数据和伪造数据。此时,G 被认为已经训练得很好,可以用来生成新的、真实的数据样本。

3.5.4 D和G的参数更新【11】

        尽管包含真实数据的数据集没有标签,但判别器(D)和生成器(G)的训练过程是以监督方式进行的。在训练的每一步,D和G的参数都会被更新。实际上,在最初的GAN提案中,D的参数会更新 k 次,而G的参数在每个训练步骤中只更新一次。然而,为了简化训练过程,你可以考虑将 k 设为1。

      【12】给出了算法的参数更新过程的推导,有兴趣可以参考:

        判别器是一个二分类器,用于区分输入是来自真实数据还是生成器生成的假数据【13,14,15】。判别器对输入x输出一个标量预测值 ,比如使用一个隐藏大小为1的全连接层,然后应用sigmoid函数得到预测概率 。假设真实数据的标签 y 为1,假数据的标签为0。训练判别器以最小化交叉熵损失,即: 。为了训练D,在每次迭代中,将从训练数据中获取一些真实样本标记为1,并将G生成的一些样本标记为0。这样就可以使用常规的监督训练框架来更新D的参数,以最小化损失函数:

        对于每批包含标记真实样本和生成样本的训练数据,需要更新D的参数,以最小化损失函数。在D的参数更新后,训练G以生成更好的样本。G的输出连接到D,D的参数保持固定。将由G和D组成的系统想象成一个单一的分类系统,该系统接收随机样本作为输入,并输出分类结果,在这种情况下可以解释为概率。

        当G足够好地“欺骗”D时,输出概率应该接近1。可以在这里使用常规的监督训练框架:用于训练由G和D组成的分类系统的数据集将由随机输入样本提供,每个输入样本的标签将是1。在训练过程中,随着D和G参数的更新,G生成的样本将更加接近真实数据,而D在区分真实数据和生成数据时会遇到更多困难。

3.5.5 GAN作为生成模型的特点

        生成对抗网络(GAN)被称为生成模型,原因在于其专注于生成新的数据样本,这些样本在统计特性上与训练数据相似。可以看到GAN和之前介绍的几个算法有一定差异,之前算法都是学习联合分布,而GAN主要是对P(X)进行建模。GAN的核心目的是生成新的数据样本。训练过程中,生成器会从一个潜在空间(通常是随机噪声)中生成数据样本,这些样本应该与真实数据类似。生成的数据可以是图像、文本、音频等。GAN通过对抗训练来提升生成样本的质量。生成器通过生成逼真的样本来欺骗判别器,而判别器则试图区分生成样本和真实样本。随着训练的进行,生成器生成的样本会越来越接近真实数据,从而成为高质量的生成模型。生成器和判别器通过对抗过程互相优化,从而无需明确的标签信息也能学习数据的生成分布。生成模型的目标是学习数据的分布并生成与这些数据分布一致的新样本。

3.5.6 代码示例

        【10】给出相应代码示例,帮助理解。在CIFAR-10图像数据集上训练,利用PyTorch创建一个生成对抗网络(GAN),在生成器和判别器训练之间切换。迭代10个训练周期(epoch),生成可视化图像,虽然还是挺模糊,但给了一定的启发。

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np

# Set device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Define a basic transform
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
train_dataset = datasets.CIFAR10(root='data', train=True, download=True, transform=transform)
dataloader = torch.utils.data.DataLoader(train_dataset, \
                                         batch_size=32, shuffle=True)

# Hyperparameters
latent_dim = 100
lr = 0.0002
beta1 = 0.5
beta2 = 0.999
num_epochs = 10


# Define the generator
class Generator(nn.Module):
    def __init__(self, latent_dim):
        super(Generator, self).__init__()

        self.model = nn.Sequential(
            nn.Linear(latent_dim, 128 * 8 * 8),
            nn.ReLU(),
            nn.Unflatten(1, (128, 8, 8)),
            nn.Upsample(scale_factor=2),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128, momentum=0.78),
            nn.ReLU(),
            nn.Upsample(scale_factor=2),
            nn.Conv2d(128, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64, momentum=0.78),
            nn.ReLU(),
            nn.Conv2d(64, 3, kernel_size=3, padding=1),
            nn.Tanh()
        )

    def forward(self, z):
        img = self.model(z)
        return img


# Define the discriminator
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()

        self.model = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=2, padding=1),
            nn.LeakyReLU(0.2),
            nn.Dropout(0.25),
            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1),
            nn.ZeroPad2d((0, 1, 0, 1)),
            nn.BatchNorm2d(64, momentum=0.82),
            nn.LeakyReLU(0.25),
            nn.Dropout(0.25),
            nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1),
            nn.BatchNorm2d(128, momentum=0.82),
            nn.LeakyReLU(0.2),
            nn.Dropout(0.25),
            nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256, momentum=0.8),
            nn.LeakyReLU(0.25),
            nn.Dropout(0.25),
            nn.Flatten(),
            nn.Linear(256 * 5 * 5, 1),
            nn.Sigmoid()
        )

    def forward(self, img):
        validity = self.model(img)
        return validity


# Define the generator and discriminator
# Initialize generator and discriminator
generator = Generator(latent_dim).to(device)
discriminator = Discriminator().to(device)
# Loss function
adversarial_loss = nn.BCELoss()
# Optimizers
optimizer_G = optim.Adam(generator.parameters() \
                         , lr=lr, betas=(beta1, beta2))
optimizer_D = optim.Adam(discriminator.parameters() \
                         , lr=lr, betas=(beta1, beta2))

# Training loop
for epoch in range(num_epochs):
    for i, batch in enumerate(dataloader):
        # Convert list to tensor
        real_images = batch[0].to(device)
        # Adversarial ground truths
        valid = torch.ones(real_images.size(0), 1, device=device)
        fake = torch.zeros(real_images.size(0), 1, device=device)
        # Configure input
        real_images = real_images.to(device)

        # ---------------------
        #  Train Discriminator
        # ---------------------
        optimizer_D.zero_grad()
        # Sample noise as generator input
        z = torch.randn(real_images.size(0), latent_dim, device=device)
        # Generate a batch of images
        fake_images = generator(z)

        # Measure discriminator's ability
        # to classify real and fake images
        real_loss = adversarial_loss(discriminator \
                                         (real_images), valid)
        fake_loss = adversarial_loss(discriminator \
                                         (fake_images.detach()), fake)
        d_loss = (real_loss + fake_loss) / 2
        # Backward pass and optimize
        d_loss.backward()
        optimizer_D.step()

        # -----------------
        #  Train Generator
        # -----------------

        optimizer_G.zero_grad()
        # Generate a batch of images
        gen_images = generator(z)
        # Adversarial loss
        g_loss = adversarial_loss(discriminator(gen_images), valid)
        # Backward pass and optimize
        g_loss.backward()
        optimizer_G.step()
        # ---------------------
        #  Progress Monitoring
        # ---------------------
        if (i + 1) % 100 == 0:
            print(
                f"Epoch [{epoch + 1}/{num_epochs}]\
                        Batch {i + 1}/{len(dataloader)} "
                f"Discriminator Loss: {d_loss.item():.4f} "
                f"Generator Loss: {g_loss.item():.4f}"
            )
    # Save generated images for every epoch
    if (epoch + 1) % 10 == 0:
        with torch.no_grad():
            z = torch.randn(16, latent_dim, device=device)
            generated = generator(z).detach().cpu()
            grid = torchvision.utils.make_grid(generated, \
                                               nrow=4, normalize=True)
            plt.imshow(np.transpose(grid, (1, 2, 0)))
            plt.axis("off")
            plt.show()

输出:

        最左边是CIFAR-10的原始真实图片,最右边是generator生成的图片,虽然迭代的次数不多,但有那么一点雏形了。

3.6 稳定扩散模型(Stable Diffusion)

        在介绍Stable Diffusion【16】模型之前,先将两个事情。一个是SD模型生成的图片,另一个是听到SD的时候,脑子里突然想到的一个复杂科学或者说是随机图理论里面的概念-渗流理论。

        先看SD生成的图片效果,非常惊艳,忍不住就想去了解SD的原理到底是啥神仙技术,哈哈。


        另外说一个题外话,看到稳定扩散模型脑子里第一时刻浮现的概念--渗流理论。

        渗流理论(Permeation Theory)与稳定扩散(Stable Diffusion)虽然在表面上看起来属于不同的领域,但它们之间感觉有一些有趣的联系。

渗流理论(Permeation Theory)

定义:渗流理论通常用于描述液体或气体在多孔介质中的流动。这种理论广泛应用于地质学、土木工程、化学工程等领域。它的核心是研究如何通过孔隙介质中的流体传播,以及影响这种传播的因素,比如介质的孔隙度、渗透率、流体的粘度等。

基本方程:在渗流理论中,最常见的方程是达西定律(Darcy's Law),它描述了流体流过多孔介质的流速与压力梯度之间的关系:

其中,是流速矢量,k 是介质的渗透率, 是压力梯度。

稳定扩散(Stable Diffusion)
        稳定扩散是一种用于生成模型的技术,特别是在图像生成和处理任务中。它是基于扩散过程的图像生成方法,模型通过逐步添加噪声并反向去噪来生成新图像。这种方法可以生成高质量的图像,并在各种应用中取得了显著的成功。稳定扩散模型其实是描述了物质图像特征在图像空间中如何扩散。

3.6.1 Stable Diffusion简介

        稳定扩散模型是一种文本到图像模型,将文本提示转换为高分辨率图像。模型首先将文本提示转换为文本表示,文本表示用于生成图像表示,图像表示表达了文本提示中描绘的图像。然后,这种图像表示被放大为高分辨率图像。稳定扩散之所以引入图像表示,而不直接生成高分辨率图像,原因还是在计算效率。在紧凑的图像表示上进行大部分计算,而不是在高分辨率图像上进行,可以减少计算时间和成本,同时保持高图像质量。图像表示从随机噪声开始,在多个时间步中被去噪,以达到一个与文本提示高度一致的高质量图像。通过将图像生成过程分解为逐步应用去噪自编码器,扩散模型(DMs)在图像数据可以实现较好的合成结果。

        【17】展示了文本输入及其生成的图像、文本和图像的结合来修改图像等的应用场景。另外给出了SD模型的框架图,稳定扩散是由多个组件和模型组成的系统,并不是一个单一的模型。

        通过对组件组成的观察,SD中有一个文本理解组件,将文本信息转换为捕捉文本思想的数值表示,这个文本编码器是一个特殊的Transformer语言模型(CLIP模型的文本编码器)。它接收输入文本,并输出表示文本中每个token的嵌入向量。这些信息随后被传递给图像生成器,图像生成器本身由几个组件组成。图像生成器经过两个阶段:

1. 图像信息生成器

这个组件是稳定扩散的核心。该组件运行多个步骤以生成图像信息。这是稳定扩散接口和库中的步骤参数,一般参数默认为50或100。图像信息生成器完全在图像信息空间中工作。这个组件由UNet神经网络和一个调度算法组成。“扩散”一词描述了这个组件中的过程。它是逐步处理噪音信息的过程,最终通过下一个组件——图像解码器,生成高质量的图像。

2. 图像解码器

图像解码器根据从信息生成器获得的信息绘制图像。它仅在过程的最后运行一次,以生成最终的像素图像。

由此,可以看到稳定扩散的三个主要组件:

ClipText(文本编码器)

输入:文本。 输出:77个标记嵌入向量,每个向量为768维。

UNet + Scheduler(信息处理器)

输入:文本嵌入和一个由噪声组成的张量。 输出:处理后的信息数组。

Autoencoder Decoder(图像解码器)

输入:处理后的信息数组(维度:(4,64,64))。 输出:生成的图像(维度:(3, 512, 512),即(红/绿/蓝,宽度,高度))。

3.6.2 模块详解

3.6.2.1 文本编码器

        文本的向量化处理,一般包含两个步骤,第一步是tokenizing、另一个是基于token转成embedding【19】。

1. 标记化(Tokenizing)

        标记化是处理文本数据的常见方法,用于将文本转换为数字,并通过神经网络进行处理。稳定扩散将文本提示标记化为一系列标记。例如,它将文本提示“a cute and adorable bunny”分割为标记“a”,“cute”,“and”,“adorable”和“bunny”。此外,为了标记提示的开始和结束,稳定扩散在标记序列的开头和结尾添加了<start>和<end>标记。上述示例的结果标记序列为<start>,a,cute,and,adorable,bunny,<end>。为了便于计算,稳定扩散将任何文本提示的标记序列保持为相同的长度77,通过填充或截断实现。如果输入提示的标记少于77个,<end>标记会被添加到序列的末尾,直到达到77个标记。如果输入提示的标记多于77个,则保留前77个标记,其余部分被截断。设置77的长度是为了平衡性能和计算效率。

2. 文本编码(Text Encoding)

        稳定扩散将标记序列转换为文本表示。为了使用文本表示来指导图像生成,稳定扩散确保文本表示包含与提示中描述的图像相关的信息。通过使用一种特殊的神经网络CLIP来实现的。CLIP由一个图像编码器和一个文本编码器组成,经过训练可以将图像及其文本描述编码成相似的向量。因此,CLIP的文本编码器计算的提示文本表示很可能包含有关提示中描述的图像的信息。

关于CLIP模型训练【17】,目标是实现文本和图像之间的跨模态理解。

3.6.2.2 扩散过程

整个扩散过程包含两部分【20】,扩散过程的数学推导,可以参考【22,23】,涉及到马尔可夫过程:

正向扩散:往图像中添加噪声。 反向扩散:去除图像中的噪声。
a. 正向扩散(Forward Diffusion)

在正向扩散过程中,图像逐步添加噪声,直到最终变成纯噪声。这个过程可以用以下公式表示:

b. 反向扩散(Reverse Diffusion)

在反向扩散过程中,从纯噪声开始,逐步去噪生成图像。这个过程使用训练好的去噪网络来预测每一步的清晰图像。公式如下:

c. 训练与生成

训练阶段:使用正向扩散过程将真实图像转换为噪声,并训练一个去噪网络来预测噪声。网络通过最小化去噪损失函数进行优化。

假设我们有一张图像,我们生成一些噪声,并将其添加到图像中。

这可以视为一个训练样本。可以使用相同的公式创建大量训练样本来训练图像生成模型的核心组件。

这个示例展示了从图像(噪声量为0,没有噪声)到总噪声(噪声量为4,完全噪声)的一些噪声量值,但可以控制添加到图像中的噪声量,因此可以在几十个步骤中分布噪声,为训练数据集中的所有图像创建几十个训练样本。

通过这个数据集,可以训练噪声预测器,并最终得到一个出色的噪声预测器,该预测器在特定配置下运行时实际上可以生成图像。

这里贴一下李宏毅老师分享的ppt中的图,帮助理解:

生成阶段:使用训练好的去噪网络,从纯噪声开始逐步生成图像,通过反向扩散过程得到最终图像。训练好的噪声预测器可以接受一张有噪声的图像和去噪步骤的数量,并能够预测出一部分噪声。采样的噪声是这样预测的,以便当我们从图像中减去它时,得到的图像更接近模型训练时所用的图像(不是确切的图像本身,而是分布)。

        目前为止描述的扩散过程生成图像时不使用任何文本数据。因此,如果部署这个模型,它会生成看起来很好的图像,但无法控制它是否是金字塔、猫或其他任何东西的图像。因此,需要将文本纳入过程,以控制模型生成的图像类型。为了使文本成为图像生成过程的一部分,需要调整噪声预测器以使用文本作为输入。数据集包括编码后的文本。由于是在潜在空间中操作,输入图像和预测的噪声都在潜在空间中。为了更好地了解文本标记如何在UNet中使用,对比分析一下没有文本和有文本的UNet差别。

没有文本的UNet噪声预测器的层

        它的输入和输出如下:

        内部结构显示:

UNet是一系列层,这些层用于转换潜在数组。 每层都在处理前一层的输出。 一些输出通过残差连接馈送到网络中的后续处理。 时间步被转换成时间步嵌入向量,这就是在各层中使用的内容。

有文本的UNet噪声预测器的层

        为了支持文本输入(也就是文本条件化),需要在ResNet块之间添加一个注意力层,这是对系统的主要改动。

在每个时间步,UNet的神经网络预测当前时间步图像表示中的噪声。UNet有三个输入:

当前时间步的图像表示 提示的文本表示,用于指导从当前图像表示中去除的噪声,以生成符合文本提示的图像 时间步,用于指示当前图像表示中剩余的噪声量

换句话说,UNet在文本提示的表示和时间步的指导下,预测当前图像表示中的条件噪声。

然而,即使我们用文本提示来条件化噪声预测,生成的图像表示通常也不会完全符合文本提示。为了提高符合度,稳定扩散通过额外预测基于空提示(" ")的通用噪声,并将其从条件化的噪声中减去来衡量提示的影响:

                                        提示的影响 = 条件化噪声 - 通用噪声

换句话说,通用噪声有助于改善图像质量,而提示的影响有助于增强与提示的符合度。最终的噪声是它们的加权和,由一个称为引导尺度的值控制:

                                        通用噪声 + 引导尺度 × 提示的影响

引导尺度为0意味着不符合文本提示,而引导尺度为1意味着使用原始的条件化噪声。较大的引导尺度会导致更强的提示符合度,但过大的值可能会降低图像质量。在Diffusion Explainer中更改引导尺度的值,观察它如何改变生成的图像。

然后,稳定扩散决定实际从图像中去除多少预测噪声,这由称为调度器的算法确定。去除少量噪声有助于逐步优化图像并生成更清晰的图像。

调度器通过考虑总时间步数来做出这个决定。然后,将降尺度的噪声从当前时间步的图像表示中减去,以获得优化后的表示,这成为下一个时间步的图像表示:

                    时间步 t+1 的图像表示 = 时间步 t 的图像表示 - 降尺度噪声

        【18,25】针对原论文中图3给出了更细致的解释:

       在Stable Diffusion中,文本的查询向量(Query)与图像特征的键向量(Key)进行匹配,以确定文本和图像之间的关系。值向量(Value)则用于调整图像生成过程中的信息。在图像生成阶段,Stable Diffusion使用UNet(一个自注意力机制的模型)来处理图像数据。UNet将图像表示和文本提示表示结合起来,通过QKV机制调整图像的细节。图像生成过程中,图像的QKV表示会与文本的QKV表示进行交互。具体来说,文本提示向量用于调整图像特征图向量,从而确保生成的图像符合文本提示的要求。这种融合方式确保了生成的图像不仅在视觉上逼真,还能准确反映文本提示的内容。

3.6.2.3 图像上采样(upscaling)

        作为扩散模型,Stable Diffusion 不同于其他图像生成模型。一般上,扩散模型使用高斯噪点对图像进行编码。然后,模型使用噪点预测器和反向扩散过程来重现图像。SD的独特之处在于它不使用图像的像素空间,它使用降低清晰度的潜在空间。  其原因是,分辨率为 512x512 的彩色图像具有 786,432 个可能的值。相比之下,SD 使用的压缩图像要小 48 倍,其值为 16,384。这就大幅度降低了处理要求。因此可以在带有 8GB RAM 的 NVIDIA GPU 的台式机上使用 Stable Diffusion。较小的潜在空间之所以可发挥作用,是因为自然图像不是随机的。SD使用解码器中的变分自动编码器(VAE)文件来绘制眼睛等精细细节。

        关于图像上采样,指的是提高数字图像分辨率的过程。它涉及通过增加更多的像素来填补低分辨率图像中的空隙,从而得到更高质量、更详细的图像。传统的上采样算法,如最近邻插值和双线性插值,通常仅基于原始像素数据进行图像的数学放大。然而,这种方法往往导致图像模糊或块状,缺乏细节和自然纹理。模型驱动的上采样模型,提供了更复杂的图像增强方法。这些模型在大量数据集上进行训练,以学习低级图像模式和语义。通过模拟现实世界的退化,高质量图像在训练过程中被人工破坏并缩小。然后,神经网络模型被训练以恢复原始图像,将大量的先验知识融入模型中。这些嵌入的知识使模型能够智能地重建缺失的信息,不仅仅是像素值。当进行图像上采样时,AI模型能够恢复锐利的边缘、自然的纹理和复杂的细节,这些可能在下采样或低分辨率捕捉过程中丢失。Stable Diffusion包含了利用AI驱动的上采样模型的功能。这个功能允许用户提升生成图像或现有低分辨率图像的质量。

3.6.3 生成模型

        以上内容介绍了SD模型的组成和计算流程。让我们再回顾一下其作为生成模型的特点。SD模型的目标是学习数据的分布,以便能够从该分布中生成新的样本。对于图像生成任务,SD模型旨在学习生成图像的概率分布,其中 x表示图像数据。SD模型尝试估计这种数据分布,以便能够从中采样生成新的图像。        

        扩散模型是一种特殊类型的生成模型,它通过逐步引入和去除噪声来生成图像。数学上,这个过程可以描述为:

前向扩散过程(Forward Diffusion Process): 在这个过程中,给定一个真实的图像 ,通过逐步添加高斯噪声,生成一系列中间噪声图像。这个过程是定义好的噪声模型 ,并且​ 通常是纯噪声。

反向生成过程(Reverse Generation Process): 生成过程通过学习从噪声图像 x_T​ 到真实图像 x_0​ 的条件概率分布 ,逐步去除噪声。模型通过优化训练目标来估计这个反向过程的参数。

        SD是条件生成模型,即模型生成图像的过程受到输入条件(如文本提示)的影响。数学上,条件生成模型的目标是学习条件分布 ,其中 c 是条件信息,例如文本描述。条件信息 c 会影响生成过程中的均值和方差,使得生成的图像能够符合输入的条件。

        SD 训练目标是最大化数据的对数似然函数,即最小化前向扩散过程的负对数似然。在实践中,这通常转化为最小化均方误差(MSE)损失,用于训练神经网络来预测噪声:

其中是实际噪声, 是由模型预测的噪声。通过这种方式,SD 能够生成新的图像样本,体现了本文所提的生成模型核心功能:从学习到的数据分布中生成新的样本。

3.6.4 使用代码示例【25】

import torch
from diffusers import StableDiffusionPipeline
from PIL import Image

# Replace the model version with your required version if needed
pipeline = StableDiffusionPipeline.from_pretrained(
    "stabilityai/stable-diffusion-2-1", torch_dtype=torch.float16
)

# Running the inference on GPU with cuda enabled
pipeline = pipeline.to('cuda')

prompt = "Photograph of a horse on a highway road at sunset"

image = pipeline(prompt=prompt).images[0]

image.show()

4. 参考材料

【1】ML | Expectation-Maximization Algorithm

【2】Hidden Markov Models

【3】Hidden Markov Models with Python

【4】Latent Dirichlet Allocation

【5】Gibbs Sampling and LDA

【6】Topic Modeling in Python: Latent Dirichlet Allocation (LDA)

【7】Goodfellow, Ian, et al. "Generative adversarial networks." Communications of the ACM 63.11 (2020): 139-144.

【8】Generative Adversarial Networks (GANs)

【9】Generative Adversarial Networks

【10】Generative Adversarial 【】Network (GAN)

【11】Generative Adverarial Networks: Build Your First Models

【12】生成对抗网络——原理解释和数学推导

【13】Understanding Generative Adversarial Networks (GANs)

【14】Could someone explain to me how back-prop is done for the generator in a GAN?

【15】GAN — What is Generative Adversarial Networks GAN?

【16】Rombach, Robin, et al. "High-resolution image synthesis with latent diffusion models." Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. 2022.

【17】The Illustrated Stable Diffusion

【18】What are Stable Diffusion Models and Why are they a Step Forward for Image Generation?

【19】Diffusion Explainer

【20】Stable Diffusion

【21】李宏毅教授Stable Diffusion模型

【22】扩散模型(Diffusion Model)——由浅入深的理解

【23】Ho, Jonathan, Ajay Jain, and Pieter Abbeel. "Denoising diffusion probabilistic models." Advances in neural information processing systems 33 (2020): 6840-6851.

【24】Image Upscaling

【25】Generate Images from Text in Python – Stable Diffusion

总结

更新时间 2024-09-21