2024年2月

当问到 Java 内存模型的时候,一定要注意,Java 内存模型(Java Memory Model,JMM)它和 JVM 内存布局(JVM 运行时数据区域)是不一样的,它们是两个完全不同的概念。

1.为什么要有 Java 内存模型?

Java 内存模型存在的原因在于解决多线程环境下并发执行时的内存可见性和一致性问题。在现代计算机系统中,尤其是多处理器架构下,每个处理器都有自己的高速缓存,而主内存(RAM)是所有处理器共享的数据存储区域。当多个线程同时访问和修改同一块共享数据时,如果没有适当的同步机制,就可能导致以下问题:

  1. 可见性
    :一个线程对共享变量所做的修改可能不会立即反映到另一个线程的视角中,因为这些修改可能只存在于本地缓存中,并未刷新回主内存。
  2. 有序性
    :编译器和处理器为了优化性能,可能会对指令进行重排序,这可能导致程序在单线程环境中看似按照源代码顺序执行,但在多线程环境中的实际执行顺序却与预期不同。
  3. 原子性
    :即使是最简单的读取或赋值操作,在硬件层面也不一定保证是原子性的,即在没有同步的情况下,多线程下可能看到操作只执行了一部分的结果。

Java 内存模型通过定义一套规则来规范并限制编译器、运行时以及处理器对内存访问的重排序行为,确保了多线程间的交互具有明确的语义。它规定了共享变量的访问规则、提供了 happens-before 原则以及 volatile 关键字、synchronized 等工具来实现内存可见性和一致性的保障。这样,程序员在编写并发代码时,可以依据这些规则来确保代码的正确执行,从而避免由于多线程带来的不确定性和错误。

如果没有 Java 内存模型就会出现以下两大问题:

  1. CPU 和 内存一致性问题。
  2. 指令重排序问题。

具体内容如下。

1.1 一致性问题

要讲明白缓存一致性问题,要从计算机的内存结构说起,它的结构是这样的:
image.png
所以从上面可以看出计算机的重要组成部分包含以下内容:

  1. CPU
  2. CPU 寄存器:也叫 L1 缓存,一级缓存。
  3. CPU 高速缓存:也叫 L2 缓存,二级缓存。
  4. (主)内存

当然,部分高端机器还有 L3 三级缓存。

由于主内存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存(L2)来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主内存中,这样就会导致多个线程在进行操作和同步时,导致 CPU 缓存和主内存数据不一致的问题。

1.2 重排序问题

由于有 JIT(Just In Time,即时编译)技术的存在,它可能会对代码进行优化,比如将原本执行顺序为 a -> b -> c 的流程,“优化”成 a -> c -> b 了,但这样优化之后,可能会导致我们的程序在某些场景执行出错,比如单例模式双重效验锁的场景,这就是典型的好心办坏事的事例。

2.定义

Java 内存模型(Java Memory Model,简称 JMM)是一种规范,它定义了 Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,即
规范了 Java 虚拟机与计算机内存之间是如何协同工作的
。具体来说,它规定了一个线程如何和何时可以看到其他线程修改过的共享变量的值,以及在必须时如何同步地访问共享变量。

3.规范内容

Java 内存模型主要包括以下内容:

  1. 主内存(Main Memory)
    :所有线程共享的内存区域,包含了对象的字段、方法和运行时常量池等数据。
  2. 工作内存(Working Memory)
    :每个线程拥有自己的工作内存,用于存储主内存中的数据的副本,线程只能直接操作工作内存中的数据。
  3. 内存间交互操作
    :线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将修改后的数据刷新到主内存。
  4. 原子性(Atomicity)
    :JMM 保证基本数据类型(如 int、long)的读写操作具有原子性,即不会被其他线程干扰,保证操作的完整性。
  5. 可见性(Visibility)
    :JMM 确保一个线程对共享变量的修改对其他线程可见。这意味着一个线程在工作内存中修改了数据后,必须将最新的数据刷新到主内存,以便其他线程可以读取到更新后的数据。
  6. 有序性(Ordering)
    :JMM 保证程序的执行顺序按照一定的规则进行,不会出现随机的重排序现象。这包括了编译器重排序、处理器重排序和内存重排序等。

Java 内存模型通过以上规则和语义,提供了一种统一的内存访问方式,使得多线程程序的行为可预测、可理解,并帮助开发者编写正确和高效的多线程代码。开发者可以利用 JMM 提供的同步机制(如关键字 volatile、synchronized、Lock 等)来实现线程之间的同步和通信,以确保线程安全和数据一致性。

内存模型的简单执行示例图如下:
image.png

3.1 主内存和工作内存交互规范

为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现(以下内容只需要简单了解即可):

  1. lock(锁定)
    :作用于主内存的变量,把一个变量标识为一条线程独占状态。
  2. unlock(解锁)
    :作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取)
    :作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
  4. load(载入)
    :作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用)
    :作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值)
    :作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储)
    :作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  8. write(写入)
    :作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。

PS:工作内存也就是本地内存的意思。

3.2 什么是 happens-before 原则?

happens-before(先行发生)原则是 Java 内存模型中定义的用于保证多线程环境下操作执行顺序和可见性的一种重要手段。

举个例子来说,例如 A happens-before B,也就是 A 线程早于 B 线程执行,那么 A happens-before B 可以保障以下两项内容:

  • 可见性
    :B 读取到 A 最新修改的值(通过内存屏障)。
  • 顺序性
    :编译器优化、处理器重排序等因素不会影响先执行 A 再执行 B 的顺序。

课后思考

JMM 和内存屏障有什么关系?happens-before 原则和内存屏障有什么关系?内存屏障的类型又有哪些?

标题党一下,顺便蹭一下 OpenAI Sora大模型的热点,主要也是回顾一下扩散模型的原理。

1. 简单理解扩散模型

简单理解,扩散模型如下图所示可以分成两部分,一个是 forward,另一个是 reverse 过程:

扩散模型简单示意图,两行图像分别表示 0、T/2、T 时刻的加噪图像和去噪图像

  • forward:这是加噪声的过程,表示为
    \(q(X_{0:T})\)
    ,即在原图(假设是
    \(t_0\)
    时刻的数据,即
    \(X_0\)
    )的基础上分时刻(一般是 T 个时刻)逐步加上噪声数据,最终得到
    \(t_T\)
    时刻的数据
    \(X_T\)
    。具体来说我们每次加一点噪声,可能加了 200 次噪声后得到服从正态分布的隐变量,即
    \(X_t=X_0+ z_0+ z_1+...+ z_{t-1}\)
    每个时刻加的噪声会作为标签用来在逆向过程的时候训练模型。
  • reverse:这很好理解,其实就是去噪过程,是
    \(q(X_{0:T})\)
    的逆过程,表示为
    \(P_\theta(X_{0:T})\)
    ,即逐步对数据
    \(X_T\)
    逆向地去噪,尽可能还原得到原图像。逆向过程其实就是需要训练一个模型来预测每个时刻的噪声
    \(z_T\)
    ,从而得到上一时刻的图像,通过迭代多次得到原始图像,即
    \(X_0=X_t-z_t-z_{t-2}-...-z_1\)
    。模型训练会迭代多次,每次的输入是当前时刻数据
    \(X_t\)
    ,输出是噪声
    \(z_t\)
    ,对应标签数据是
    \(\overline z_{t-1}\)
    ,损失函数是
    \(mse(z_t,\overline z_{t-1})\)

怎么理解这两个过程呢?一种简单的理解方法是我们可以假设世界上所有图像都是可以通过加密(就是 forward 过程)表示成隐变量,这些隐变量人眼看上去就是一堆噪声点。我们可以通过神经网络模型逐渐把这些噪声去掉,从而得到对应的原图(即 reverse 过程)。

2. 前向过程的数学表示

扩散模型前向过程

前向过程简单理解就是不断加噪声,加噪声的特点是越加越多:

  • 前期加的噪声要少一点,这样是为了避免加太多噪声会导致模型不太好学习;
  • 而当噪声量加的足够多后应该增加噪声的量,因为如果还是每次只加一点点,其实差别不大,而且这会导致前向过程太长,那么对应逆向过程也长,最终会增加计算量。所以噪声的量会有超参数
    \(\beta_t\)
    控制。t 越大,
    \(\beta_t\)
    的值也就越大。

那我们可以很自然地知道,t 时刻的图像应该跟 t-1时刻的图像和噪声相关,所以有

\[X_t=\sqrt{\alpha_t}X_{t-1}+\sqrt{1-\alpha_t}z_1
\]

其中
\(\alpha_t=1-\beta_t\)
,
\(z_1\)
是服从 (0,1) 正太分布的随机变量。常见的参数设置是
\(\beta_t\)
从 0.0001 逐渐增加到0.002,所以
\(\alpha_t\)
对应越来越小,也就是说噪声的占比逐渐增大。

我们同样有
\(X_{t-1}=\sqrt{\alpha_{t-1}}X_{t-2}+\sqrt{1-\alpha_{t-1}}z_2\)
,此时我们有

\[\begin{align}
X_{t}\,&=\,{\sqrt{a_{t}}}({\sqrt{a_{t-1}}}X_{t-2}+{\sqrt{1-\alpha_{t-1}}}z_{2})+{\sqrt{1-\alpha_{t}}}z_1 \\
&=\sqrt{a_{t}a_{t-1}}X_{t-2}+(\sqrt{(a_{t}(1-\alpha_{t-1})}z_{2}+\sqrt{1-\alpha_{t}}z_{1}) \\
&= \sqrt{a_{t}a_{t-1}}X_{t-2}+\sqrt{1-\alpha_t\alpha_{t-1}}z_2 \\
&= \sqrt{a_{t}a_{t-1}}X_{t-2}+\tilde{z}_2 \notag
\end{align}
\]

因为
\(z_1,z_2\)
都服从正太分布,且
\(\mathcal{N}(0,\sigma_{1}^{2})+\mathcal{N}(0,\sigma_{2}^{2})\sim\mathcal{N}(0,(\sigma_{1}^{2}+\sigma_{2}^{2}))\)
,所以公式(2)的括号内的两项之和得到一个新的服从均值为 0, 方差是
\(\sqrt{(a_{t}(1-\alpha_{t-1})}^2+\sqrt{1-\alpha_{t}}^2=1-\alpha_t\alpha_{t-1}\)
的变量
\(\tilde z_2\sim\mathcal{N}(0,1-\alpha_t\alpha_{t-1})\)

我们不断递归能够得到
\(X_t\)

\(X_0\)
的关系如下:

\[\begin{align}
X_t&=\sqrt{\overline{\alpha}_t}X_0+\overline{z}_t \\
&=\sqrt{\overline{\alpha}_t}X_0+\sqrt{1-\overline{\alpha}_t}{z}_t
\end{align}
\]

其中
\(\overline{\alpha}_t=\alpha_t\alpha_{t-1}...\alpha_{1}\)
,
\(\overline{z}_t\)
是均值为 0,方差
\(\sigma=1-\overline{\alpha}_t\)
的高斯变量,
\(z_t\)
服从(0,1)正态分布。可以看到给定0 时刻的图像数据
\(X_0\)
,我们可以求得任意t时刻的
\(\overline{\alpha}_t\)
和与之有关的
\(\overline z_t\)
,进而得到对应的
\(X_t\)
数据,至此前向过程就结束了。

3. 逆向过程的数学表示

3.1 贝叶斯公式求解

扩散模型在应用的时候主要就是 reverse 过程,即给定一组随机噪声,通过逐步的还原得到想要的图像,可以表示为
\(q(X_0|X_t)\)
。但是很显然,我们无法直接从 T 时刻还原得到 0 时刻的数据,所以退而求其次,先求
\(q(X_{t-1}|X_t)\)
。但是这个也没那么容易求得,但是由贝叶斯公式我们可以知道

\[q(X_{t-1}|X_t)=\frac{q(X_t|X_{t-1})q(X_{t-1})}{q(X_t)}
\]

我们这里考虑扩散模型训练过程,我们默认是知道
\(X_o\)
的,所以有

\[q(X_{t-1}|X_t,X_0)=\frac{q(X_t|X_{t-1},X_0)q(X_{t-1}|X_0)}{q(X_t|X_0)}
\]

解释一下上面的公式:因为我们可以人为设置噪声分布,所以正向过程中每个时刻的数据也是知道的。例如,假设噪声
\(z\)
是服从高斯分布的,那么
\(X_1=X_0+z\)
,所以
\(q(X_1,X_0)\)
是可以知道的,同样
\(q(X_{t-1},X_0),q(X_t,X_0)\)
也都是已知的,更一般地,
\(q(X_t|X_{t-1},X_0)\)
也是已知的。所以上面公式的右边三项都是已知的,要计算出左边的结果,就只需要分别求出右边三项的数学表达式了。


上面三个公式是推导后的结果,省略了亿些步骤,我们待会解释怎么来的,这里先简单解释一下含义,我们看第一行,
\(z\)
就是服从正态分布(均值为 0,方差为 1)的变量,为方便理解其它的可以看成常数,我们知道
\(a+\sqrt{b}z\)
会得到均值为 a,方差为 b 的服从高斯分布的变量,那么第一行最右边的高斯分布应该就好理解了。其余两行不做赘述,同理。

3.2 高斯分布概率密度分布计算

下面公式中左边的概率分布其实就是右边三项概率分布的计算结果。

\[q(X_{t-1}|X_t,X_0)=\frac{q(X_t|X_{t-1},X_0)q(X_{t-1}|X_0)}{q(X_t|X_0)}
\]

我们假设了噪声数据服从高斯分布
\(\mathcal{N}(\mu,\sigma^2)\)
,并且知道高斯分布的概率密度函数是
\(exp{(-\frac{1}{2}\frac{(x-\mu)^2}{\sigma^2})}\)
。结合上面已经给出的三项的高斯分布情况,例如

q(X_t|X_0)
我们可以求得
\(q(X_t|X_0)\)
的概率密度函数为
\(exp(-\frac{1}{2}\frac{(X_t-\sqrt{\overline{a_t}}X_0)^2}{1-\overline{a_t}})\)
,其它两项同理,它们计算后得到的最终的概率密度函数为:

\[\propto\exp\left(-\,\frac{1}{2}\,(\frac{({X}_{t}-\sqrt{\alpha_{t}}{X}_{t-1})^{2}}{\beta_{t}}+\frac{({X}_{t-1}-\sqrt{\alpha}_{t-1}{X}_{0})^{2}}{1-\overline{{{\alpha}}}_{t-1}}-\frac{({X}_{t}-\sqrt{\overline{{{\alpha}}}_{t}}{X}_{0})^{2}}{1-\overline{{{\alpha}}}_{t}})\right)
\]

其中上面公式中
\(\beta_t=1-\alpha_t\)
。接着我们把上面公式的平方项展开,以
\(X_{t-1}\)
为变量(因为此时我们的目的是求得
\(X_{t-1}\)
)合并同类项整理一下最后可以得到


我们在对比一下
\(exp{(-\frac{1}{2}\frac{(x-\mu)^2}{\sigma^2})}=exp(-\frac{1}{2}(\frac{1}{\sigma^2}x^2-\frac{2\mu}{\sigma^2}x+\frac{\mu^2}{\sigma^2}))\)
就能知道上面公式中对应的方差和均值:

  • 方差

\[\tilde\sigma_t^2=\frac{1-\overline{\alpha}_{t-1}}{1-\overline{\alpha}_t}\beta_t
\]

方差等式中的
\(\alpha,\beta\)
都是与分布相关的固定值,即给定高斯分布后,这些变量的值是固定的,所以方差是固定值。

  • 均值

\[\tilde{\mu}_{t}({X}_{t},{X}_{0})\;=\frac{\sqrt{\alpha_{t}}({\bf1}-\bar{\alpha}_{t-1})}{{\bf1}-\bar{\alpha}_{t}}{X}_{t}+\frac{\sqrt{\bar{\alpha}_{t-1}}\beta_{t}}{{\bf1}-\bar{\alpha}_{t}}{X}_{0}
\]

均值跟
\(X_t\)

\(X_0\)
有关 ,但是此时的已知量是
\(X_t\)
,而
\(X_0\)
是未知的。不过我们可以估计一下
\(X_0\)
的值,通过前向过程我们知道
\(X_t=\sqrt{\overline{a}_t}X_0+\sqrt{1-\overline{a}_t}z_t\)
,那么可以逆向估计一下
\(X_0=\frac{1}{\sqrt{\overline{a}_t}}(X_t-\sqrt{1-\overline{a}_t}z_t)\)
。不过需要注意的是,这里的
\(X_0\)
只是通过
\(X_t\)
估算得到的,并不是真实值。所以均值表达式还可以进一步简化,即

\[\tilde{\mu}_{t}=\frac{1}{\sqrt{a_{t}}}(X_{t}-\frac{\beta_{t}}{\sqrt{1-\bar{a}_{t}}}{z}_{t})
\]

每个时刻的均值和方差的表达式就都有了。不过,每个时刻的方差是个定值,很容易求解,而均值却跟变量
\(z_t\)
相关。如果能求解得到
\(z_t\)
,那么只要给定一个t 时刻的随机噪声填满的图像
\(X_t\)
,我们就能知道该时刻噪声的均值和方差,那么我们就可以通过采样得到上一时刻的噪声数据

\[X_{t-1}=\tilde\mu_t+\tilde\sigma_t \epsilon
\]

\(\epsilon\)
是服从(0,1)的正态分布的随机变量。至此,我们只需要引入神经网络模型来预测 t 时刻的
\(z_t\)
,即
\(z_t=\text{diffusion_model}(x_t)\)
,模型训练好后就能得到前一时刻的
\(X_{t-1}\)
了。

那么要训练模型,我们肯定得有标签和损失函数啊。具体而言:

  • \(x_t\)
    是模型的输入
  • \(z_t\)
    就是模型的输出
  • 标签其实就是 forward 过程中每个时刻产生的噪声数据
    \(\hat{z}_t\)
  • 所以损失函数等于
    \(\text{loss}=mse(z_t, \hat{z}_t)\)

4. 代码实现

接下来我们结合代码来理解一下上述过程。

4.1 前向过程(加噪过程)

给定原始图像
\(X_0\)
和加噪的超参数
\(\alpha_t=1-\beta_t\)
可以求得任意时刻对应的加噪后的数据
\(X_t\)
,即

\[\begin{align}
X_t&=\sqrt{\overline{\alpha}_t}X_0+\overline{z}_t\\
&=\sqrt{\overline{\alpha}_t}X_0+\sqrt{1-\overline{\alpha}_t}{z}_t
\end{align}
\]

其中
\(\overline{\alpha}_t=\alpha_t\alpha_{t-1}...\alpha_{1}\)
,
\(\overline{z}_t\)
是均值为 0,标准差
\(\sigma=\sqrt{1-\overline{\alpha}_t}\)
的高斯变量。

下面是具体的代码实现,首先是与噪声相关超参数的设置和提前计算:

from PIL import Image
import matplotlib.pyplot as plt

import torch
import torch.nn.functional as F
from torchvision import transforms

# 定义线性beta时间表
def linear_beta_schedule(timesteps, start=0.0001, end=0.02):
    # 在给定的时间步数内,线性地从 start 到 end 生成 beta 值
    return torch.linspace(start, end, timesteps)

T = 300  # 总的时间步数
betas = linear_beta_schedule(timesteps=T) # β,迭代100个时刻

# 预计算不同的超参数(alpha和beta)
alphas = 1.0 - betas
alphas_cumprod = torch.cumprod(alphas, axis=0)  # 累积乘积
alphas_cumprod_prev = F.pad(alphas_cumprod[:-1], (1, 0), value=1.0)  # 前一个累积乘积
sqrt_recip_alphas = torch.sqrt(1.0 / alphas)  # alpha的平方根倒数
sqrt_alphas_cumprod = torch.sqrt(alphas_cumprod)  # alpha累积乘积的平方根
sqrt_one_minus_alphas_cumprod = torch.sqrt(1.0 - alphas_cumprod)  # 1-alpha累积乘积的平方根
posterior_variance = betas * (1.0 - alphas_cumprod_prev) / (1.0 - alphas_cumprod) # 计算后验分布q(x_{t-1}|x_t,x_0)的方差

接下来是具体的前向过程的计算,其中
get_index_from_list
函数是为了快速获得指定 t 时刻对应的超参数的值,支持批量图像操作。
forward_diffusion_sample
则是前向扩散采样函数。

def get_index_from_list(vals, time_step, x_shape):
    """
    返回传入的值列表vals(如β_t 或者α_t)中特定时刻t的值,同时考虑批量维度。
    参数:
    vals: 一个张量列表,包含了不同时间步的预计算值。
    time_step: 一个包含时间步的张量,其值决定了要从vals中提取哪个时间步的值。
    x_shape: 原始输入数据的形状,用于确保输出形状的一致性。
    
    返回:
    一个张量,其形状与原始输入数据x_shape相匹配,但是在每个批次中填充了特定时间步的vals值。
    """
    batch_size = time_step.shape[0]  # 获取批量大小
    out = vals.gather(-1, time_step.cpu())  # 从vals中按照时间步收集对应的值
    # 重新塑形为原始数据的形状,确保输出与输入在除批量外的维度上一致
    return out.reshape(batch_size, *((1,) * (len(x_shape) - 1))).to(time_step.device)


# 前向扩散采样函数
def forward_diffusion_sample(x_0, time_step, device="cpu"):
    """
    输入:一个图像和一个时间步
	返回:图像对应时刻的噪声版本数据
    """
    noise = torch.randn_like(x_0)  # 生成和x_0形状相同的噪声
    sqrt_alphas_cumprod_t = get_index_from_list(sqrt_alphas_cumprod, time_step, x_0.shape)
    sqrt_one_minus_alphas_cumprod_t = get_index_from_list(sqrt_one_minus_alphas_cumprod, time_step, x_0.shape)
    # 计算均值和方差
    return sqrt_alphas_cumprod_t.to(device) * x_0.to(device) + sqrt_one_minus_alphas_cumprod_t.to(device) * noise.to(
        device
    ), noise.to(device)

image = Image.open('xiaoxin.jpg').convert('RGB')
img_tensor = transforms.ToTensor()(image)

for idx in range(T):
	time_step = torch.Tensor([idx]).type(torch.int64)
	img, noise = forward_diffusion_sample(img_tensor, time_step)
	plt.imshow(transforms.ToPILImage()(img)) # 绘制加噪图像

4.2 训练

训练过程
我们忽略具体的模型结构细节,先看看训练流程是怎样的:

if __name__ == "__main__":
    model = SimpleUnet()
    T = 300
    BATCH_SIZE = 128
    epochs = 100

    dataloader = load_transformed_dataset(batch_size=BATCH_SIZE)

    device = "cuda" if torch.cuda.is_available() else "cpu"
    logging.info(f"Using device: {device}")
    model.to(device)
    optimizer = Adam(model.parameters(), lr=0.001)

    for epoch in range(epochs):
        for batch_idx, (batch_data, _) in enumerate(dataloader):
            optimizer.zero_grad()
			
			# 对一个 batch 内的数据采样任意时刻的 time_step
            t = torch.randint(0, T, (BATCH_SIZE,), device=device).long() 
			x_noisy, noise = forward_diffusion_sample(batch_data, t, device) # 计算得到指定时刻的 加噪后的数据 和 对应的噪声数据
    		noise_pred = model(x_noisy, t) # 预测对应时刻的噪声
			loss = F.mse_loss(noise, noise_pred) # 计算噪声预测的损失值
            loss.backward()
            optimizer.step()

这里我们忽略模型架构的具体细节,只需要知道每次模型的计算需要 噪声图像(
x_noisy
) 和 对应的时刻
t
即可。

4.2 逆向过程(去噪采样过程)

采样过程

给定某一时刻的数据
\(X_t\)
,该时刻的均值
\(\mu\)
和方差
\(\sigma\)
如下

\[\tilde{\mu}_{t}=\frac{1}{\sqrt{a_{t}}}(X_{t}-\frac{\beta_{t}}{\sqrt{1-\bar{a}_{t}}}{z}_{t})
\]

\[\tilde\sigma_t^2=\frac{1-\overline{\alpha}_{t-1}}{1-\overline{\alpha}_t}\beta_t
\]

通过对
\(\mathcal{N}(\tilde\mu_t,\tilde\sigma_t^2)\)
分布进行采样得到上一时刻的数据
\(X_{t-1}=\tilde\mu_t+\tilde\sigma_t\epsilon\)

\(z_t\)
是模型训练收敛后,在给定噪声图像和对应时刻 t 后计算得到的噪声数据,
\(\epsilon\)
是正态分布随机变量。

实现代码如下:

@torch.no_grad()
def sample_timestep(model, x, t):
    """
    使用模型预测图像中的噪声,并返回去噪后的图像。
    如果不是最后一个时间步,则在此图像上应用噪声。
    
    参数:
    model - 预测去噪图像的模型
    x - 当前带噪声的图像张量
    t - 当前时间步的索引(整数或者整数型张量)
    
    返回:
    去噪后的图像张量,如果不是最后一步,返回添加了噪声的图像张量。
    """
    # 从预设列表中获取当前时间步的beta值
    betas_t = get_index_from_list(betas, t, x.shape)
    # 获取当前时间步的累积乘积的平方根的补数
    sqrt_one_minus_alphas_cumprod_t = get_index_from_list(sqrt_one_minus_alphas_cumprod, t, x.shape)
    # 获取当前时间步的alpha值的平方根的倒数
    sqrt_recip_alphas_t = get_index_from_list(sqrt_recip_alphas, t, x.shape)

    # 调用模型来预测噪声并去噪(当前图像 - 噪声预测)
    model_mean = sqrt_recip_alphas_t * (x - betas_t * model(x, t) / sqrt_one_minus_alphas_cumprod_t)
    # 获取当前时间步的后验方差
    posterior_variance_t = get_index_from_list(posterior_variance, t, x.shape)

    if t == 0:
        # 如Luis Pereira在YouTube评论中指出的,论文中的时间步t有偏移
        return model_mean
    else:
        # 生成与x形状相同的随机噪声
        noise = torch.randn_like(x)
        # 返回模型均值加上根据后验方差缩放的噪声
        return model_mean + torch.sqrt(posterior_variance_t) * noise

for i in reversed(range(0, T)):
	t = torch.tensor([i], device='cpu', dtype=torch.long)
	img = sample_timestep(model, img, t)

5. 总结

  • 前向过程:

给定原始图像
\(X_0\)
和加噪的超参数
\(\alpha_t=1-\beta_t\)
可以求得任意时刻对应的加噪后的数据
\(X_t\)
,即

\[\begin{align}
X_t&=\sqrt{\overline{\alpha}_t}X_0+\overline{z}_t\\
&=\sqrt{\overline{\alpha}_t}X_0+\sqrt{1-\overline{\alpha}_t}{z}_t
\end{align}
\]

其中
\(\overline{\alpha}_t=\alpha_t\alpha_{t-1}...\alpha_{1}\)
,
\(\overline{z}_t\)
是均值为 0,标准差
\(\sigma=\sqrt{1-\overline{\alpha}_t}\)
的高斯变量。

  • 逆向过程

给定某一时刻的数据
\(X_t\)
,该时刻的均值
\(\mu\)
和方差
\(\sigma\)
如下

\[\tilde{\mu}_{t}=\frac{1}{\sqrt{a_{t}}}(X_{t}-\frac{\beta_{t}}{\sqrt{1-\bar{a}_{t}}}{z}_{t})
\]

\[\tilde\sigma_t^2=\frac{1-\overline{\alpha}_{t-1}}{1-\overline{\alpha}_t}\beta_t
\]

通过对
\(\mathcal{N}(\tilde\mu_t,\tilde\sigma_t^2)\)
分布进行采样得到上一时刻的数据
\(X_{t-1}=\tilde\mu_t+\tilde\sigma_t\epsilon\)

\(z_t\)
是模型训练收敛后,在给定噪声图像和对应时刻 t 后计算得到的噪声数据,
\(\epsilon\)
是正态分布随机变量。迭代 t 次后即可得到 0 时刻的图像了。

参考

微信公众号:AutoML机器学习

MARSGGBO

原创

如有意合作或学术讨论欢迎私戳联系~
邮箱:marsggbo@foxmail.com


前言

在Excel 中,依赖列表或级联下拉列表表示两个或多个列表,其中一个列表的项根据另一个列表而变化。依赖列表通常用于Excel的业务报告,例如学术记分卡中的【班级-学生】列表、区域销售报告中的【区域-国家/地区】列表、人口仪表板中的【年份-区域】列表以及生产摘要报告中的【单位-行-产品】列表等等。

在本博客中,小编将为大家介绍如何借助葡萄城公司基于 .NET 和 .NET Core 平台的服务端高性能表格组件组件GrapeCity Documents for Excel (以下简称GcExcel)和动态数组函数 UNIQUE、CHOOSECOLS 和 FILTER 以编程方式创建主列表和依赖下拉列表。

背景需求

下图是一张某公司的客户订单表原始数据:

现在为了将这些数据按照人名分类进行查阅,小编需要制作两个下拉列表(客户姓名和订单ID),同时需要满足订单ID的值是与客户姓名相关的,然后最下面显示的是根据订单ID查询出来的订单详细信息,如下图所示:

使用GcExcel实现的步骤

步骤 1 - 工作簿初始化

使用 GcExcel API,第一步是初始化 Workbook 的实例。然后,您可以根据业务需求选择打开现有 Excel 文档或创建新工作簿。在此博客中,我们将使用带有 IWorkbook 接口的 API 加载包含客户订单历史记录的现有 Excel 文档,如下所示:

Workbook workbook = new Workbook();
workbook.Open("E:\\download\\smartdependentlist\\CustomerOrderHistory.xlsx");

步骤 2 - 获取工作表

接下来,您需要获取用于创建所需报告的工作表。使用 GcExcel,可以使用 IWorkbook 界面中的 API 获取工作表。您也可以选择创建一个新的工作表。但是,为了简化报表中使用的公式,我们将在存储订单历史记录的同一工作表上创建报表,如下所示:

IWorksheet worksheet;
worksheet = workbook.Worksheets["data"]; //OR workbook.Worksheets[0];

步骤 3 - 获取客户名称的唯一列表(用于主下拉列表)

初始化后,需要获取要添加到报表中“选择客户名称”部分的主下拉列表的唯一客户名称列表。为此,请选择工作表中底部有空格的任何单元格以垂直溢出数据;我们使用了单元格T3。接下来,对所需的客户名称数据范围使用 UNIQUE 函数。

使用 GcExcel,可以使用带有 IWorksheet 接口的 API 获取单元格或单元格区域,并使用 IRange 接口的 API为其设置动态公式,如下所示:

IRange rngUniqueCustomerNames;
rngUniqueCustomerNames = worksheet.Range["T3"]; //dummy cell to get unique list of customer names
rngUniqueCustomerNames.Formula2 = "=UNIQUE($B$2:$B$2156)";

执行结果如下:

步骤 4 - 创建主下拉列表

获得客户名称列表后,将其用作使用“列表上的数据验证”创建的主下拉列表的源。在此博客示例中,此主下拉列表在单元格 L3 中创建。

使用 GcExcel,使用 IRange 接口的 API 在某个范围内配置数据验证。使用 IValidation 接口的 API 为区域添加新的验证规则实例。选择 ValidationType.List 列表类型数据验证选项,并使用 UNIQUE 公式将公式设置为单元格;这里是 T3,如下图所示:

IValidation listValidation = worksheet.Range["L3"].Validation;
listValidation.Add(ValidationType.List, ValidationAlertStyle.Stop, ValidationOperator.Equal,"=$T$3#");

请注意,要获得动态数组函数的结果范围,单元格引用后跟一个#请注意,要获得动态数组函数的结果范围,单元格引用后跟一个
#。

步骤 5 - 获取唯一 OrderID 列表(用于依赖下拉列表)

准备好主下拉列表后,让我们获取在主下拉列表中选择的客户名称的唯一 OrderID 列表。为此,请再次选择工作表中的任何单元格(在此示例中,此单元格为 $V$2)。在此单元格中使用以下公式获取所需的 OrderID 列表。

=CHOOSECOLS(
    FILTER(
        Unique_Cus_Order_combo,
        CHOOSECOLS(Unique_Cus_Order_combo,2)=CustomerName
    ),
    1
)

公式解析如下:

  1. 定义 CustomerName是指包含主下拉列表的单元格的值;在此示例中,它指的是 =$L$3

  2. 定义的Unique_Cus_Order_combo是指订单 ID 和客户名称的唯一组合范围。它存储公式 =UNIQUE(data!$A$2:$B$2156),其中范围 A 和 B 分别包含 OrderID 和 Customer Names。

返回的数据部分如下图所示:

2.内部 CHOOSECOLS 函数提供由 Unique_Cus_Order_combo 表示的范围内的 Customer 名称列表,以便与 FILTER 函数中的 CustomerName 匹配。

3.FILTER函数从所选客户名称对应的Unique_Cus_Order_combo中筛选出数据,如下图所示:

4.最后,外部 CHOOSECOLS 函数从筛选的范围内返回所需的 OrderID 列表,如下所示:

要使用 GcExcel 设置定义的名称和动态公式,请按照以下示例代码进行操作:

 workbook.Names.Add("CustomerName", "=$L$3");
 workbook.Names.Add("Unique_Cus_Order_combo", "=UNIQUE(data!$A$2:$B$2156)");

IRange rngUniqueOrderIds;
rngUniqueOrderIds = worksheet.Range["V2"]; //dummy range to get unique list of customer names
rngUniqueOrderIds.Formula2 = "=CHOOSECOLS(FILTER(Unique_Cus_Order_combo, CHOOSECOLS(Unique_Cus_Order_combo,2)=CustomerName), 1)";

步骤 6 - 填充依赖下拉列表

下一步是使用上一步中提取的列表填充 OrderID 下拉列表(在此示例中,它位于 L6)。为此,请添加类型列表的数据验证(与为主下拉列表添加的数据验证相同),并将其源值设置为包含上一步中公式的单元格值(即 =$V$2)前缀为 #。

IValidation orderIdList = worksheet.Range["L6"].Validation;
orderIdList.Add(ValidationType.List, ValidationAlertStyle.Stop, ValidationOperator.Equal, "=$v$2#");

步骤 7 - 将默认值设置为下拉列表并保存工作簿

最后,使用 IRange 接口的 API将默认值设置为下拉列表,并使用 IWorkbook 接口的 API保存工作簿,如下面的代码片段所示:

worksheet.Range["L3"].Value = "Paul Henriot";
worksheet.Range["L6"].Value = 10248;
workbook.Save("E:\\download\\smartdependentlist\\CustomerOrderHistoryReport.xlsx");

生成的带有智能依赖列表的 Excel 文件如下图所示:

总结

以上就是使用C#生成依赖列表的全过程,如果您想了解更多信息,欢迎
点击这里
查看更多资料。

扩展链接:

轻松构建低代码工作流程:简化繁琐任务的利器

优化预算管理流程:Web端实现预算编制的利器

如何在.NET电子表格应用程序中创建流程图

GPT-SoVITS是少有的可以在MacOs系统下训练和推理的TTS项目,虽然在效率上没有办法和N卡设备相提并论,但终归是开发者在MacOs系统构建基于M系列芯片AI生态的第一步。

环境搭建

首先要确保本地环境已经安装好版本大于6.1的FFMPEG软件:

(base) ➜  ~ ffmpeg -version  
ffmpeg version 6.1.1 Copyright (c) 2000-2023 the FFmpeg developers  
built with Apple clang version 15.0.0 (clang-1500.1.0.2.5)  
configuration: --prefix=/opt/homebrew/Cellar/ffmpeg/6.1.1_3 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags='-Wl,-ld_classic' --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libaribb24 --enable-libbluray --enable-libdav1d --enable-libharfbuzz --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopenvino --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox --enable-audiotoolbox --enable-neon  
libavutil      58. 29.100 / 58. 29.100  
libavcodec     60. 31.102 / 60. 31.102  
libavformat    60. 16.100 / 60. 16.100  
libavdevice    60.  3.100 / 60.  3.100  
libavfilter     9. 12.100 /  9. 12.100  
libswscale      7.  5.100 /  7.  5.100  
libswresample   4. 12.100 /  4. 12.100  
libpostproc    57.  3.100 / 57.  3.100

如果没有安装,可以先升级HomeBrew,随后通过brew命令来安装FFMPEG:

brew cleanup && brew update

安装ffmpeg

brew install ffmpeg

随后需要确保本地已经安装好了conda环境:

(base) ➜  ~ conda info  
  
     active environment : base  
    active env location : /Users/liuyue/anaconda3  
            shell level : 1  
       user config file : /Users/liuyue/.condarc  
 populated config files : /Users/liuyue/.condarc  
          conda version : 23.7.4  
    conda-build version : 3.26.1  
         python version : 3.11.5.final.0  
       virtual packages : __archspec=1=arm64  
                          __osx=14.3=0  
                          __unix=0=0  
       base environment : /Users/liuyue/anaconda3  (writable)  
      conda av data dir : /Users/liuyue/anaconda3/etc/conda  
  conda av metadata url : None  
           channel URLs : https://repo.anaconda.com/pkgs/main/osx-arm64  
                          https://repo.anaconda.com/pkgs/main/noarch  
                          https://repo.anaconda.com/pkgs/r/osx-arm64  
                          https://repo.anaconda.com/pkgs/r/noarch  
          package cache : /Users/liuyue/anaconda3/pkgs  
                          /Users/liuyue/.conda/pkgs  
       envs directories : /Users/liuyue/anaconda3/envs  
                          /Users/liuyue/.conda/envs  
               platform : osx-arm64  
             user-agent : conda/23.7.4 requests/2.31.0 CPython/3.11.5 Darwin/23.3.0 OSX/14.3 aau/0.4.2 s/XQcGHFltC5oP5DK5UVaTDA e/E37crlCLfv4OPFn-Q0QPJw  
                UID:GID : 502:20  
             netrc file : None  
           offline mode : False

如果没有安装过conda,推荐去anaconda官网下载安装包:

https://www.anaconda.com

接着通过conda命令创建并激活基于3.9的Python开发虚拟环境:

conda create -n GPTSoVits python=3.9  
conda activate GPTSoVits

安装依赖以及Mac版本的Torch

克隆GPT-SoVits项目:

https://github.com/RVC-Boss/GPT-SoVITS.git

进入项目:

cd GPT-SoVITS

安装基础依赖:

pip3 install -r requirements.txt

安装基于Mac的Pytorch:

pip3 install --pre torch torchaudio --index-url https://download.pytorch.org/whl/nightly/cpu

随后检查一下mps是否可用:

(base) ➜  ~ conda activate GPTSoVits  
(GPTSoVits) ➜  ~ python  
Python 3.9.18 (main, Sep 11 2023, 08:25:10)   
[Clang 14.0.6 ] :: Anaconda, Inc. on darwin  
Type "help", "copyright", "credits" or "license" for more information.  
>>> import torch  
>>> torch.backends.mps.is_available()   
True  
>>>

如果没有问题,那么直接在项目目录运行命令进入webui即可:

python3 webui.py

到底用CPU还是用MPS

在推理环节上,有个细节非常值得玩味,那就是,到底是MPS效率更高,还是直接用CPU效率更高,理论上当然是MPS了,但其实未必,我们可以修改项目中的config.py文件来强行指定api推理设备:

if torch.cuda.is_available():  
    infer_device = "cuda"  
elif torch.backends.mps.is_available():  
    infer_device = "mps"  
else:  
    infer_device = "cpu"

或者修改inference_webui.py文件来指定页面推理设备:

if torch.cuda.is_available():  
    device = "cuda"  
elif torch.backends.mps.is_available():  
    device = "mps"  
else:  
    device = "cpu"

基于cpu的推理效率:

CPU推理时Python全程内存占用3GB,内存曲线全程绿色,推理速度长时间保持55it/s。

作为对比,使用MPS进行推理,GPU推理时,Python进程内存占用持续稳步上升至14GB,推理速度最高30it/s,偶发1-2it/s。

但实际上,在Pytorch官方的帖子中:

https://github.com/pytorch/pytorch/issues/666666517

提到了解决方案,即修改cmakes的编译方式。

修改后推理对比:

cpu推理:  
  
['zh']  
 19%|███████▍                                | 280/1500 [00:12<00:47, 25.55it/s]T2S Decoding EOS [102 -> 382]  
 19%|███████▍                                | 280/1500 [00:12<00:56, 21.54it/s]

gpu推理:  
  
 21%|████████▌                               | 322/1500 [00:08<00:32, 36.46it/s]T2S Decoding EOS [102 -> 426]  
 22%|████████▋                               | 324/1500 [00:08<00:29, 39.26it/s]  
  

但MPS方式确实有内存泄露的现象。

什么是订单履约系统?

订单履约系统用来管理从接到销售订单,到把货品送到客户手中的整个业务过程。它是上游交易(如销售和客户下单环节)和下游仓储配送(如库存管理、物流)之间的桥梁,确保信息流的顺畅和操作的协同,提升整个供应链的效率和响应速度。

订单履约的主要流程包括接收订单、占用库存、拣货、打包、运输以及交付等环节。

系统定位

订单履约系统的实现目标是通过技术手段,让订单处理过程更高效、更透明,从而提升客户体验。不仅要快速准确地处理订单,还要给客户提供订单状态和物流信息的实时更新。通过确保每一笔订单都能及时、无误地完成,进而提高库存管理和物流配送的效率,降低运营成本,提升客户满意度。

业务流程

订单履约过程是一系列精细协作的流程,从客户在销售平台下单开始,至商品交付用户手中结束。此过程跨越多个环节,包括消费者在销售平台下单、订单履约系统接收订单、预占库存,以及仓库和门店执行拣货、发货处理等环节。每一步都必须无缝衔接,确保整个流程高效运转。业务流程的关键在于各系统间的信息流顺畅传递,任何环节的延误都可能影响整体履约时效,进而影响客户满意度和对企业的信任。

  • 接收订单
    : 当客户在销售平台下单后,第一步是接收订单,它涉及到订单信息的收集和确认,包括客户详情、商品信息、配送地址等。此阶段,系统会验证订单的有效性,确保订单信息完整且准确。

  • 订单拆单
    : 此环节的目的是将复杂的订单拆解为更易管理的子订单,根据商品的仓储位置或特定的履约要求进行拆分,例如,需要从不同地方发货的多种商品、预订预售的商品等场景。拆单有助于提高处理效率,减少物流成本,并加速订单的处理过程。通过将大订单分解为小订单,每个子订单可以根据需求和最佳履约路径进行优化处理。

  • 派单
    : 这一步骤基于多种因素进行决策,包括商品的实际库存位置、配送地址的距离、以及履约能力,分配给合适的仓库或门店。派单的目标是确保订单能够以最高效的方式进行处理,同时考虑配送成本和配送速度,以实现对客户的承诺。

  • 预占库存
    : 为了确保订单中的商品能够被及时送达,预占库存防止在订单处理期间商品被其他订单占用,导致无法履约的情况发生。这个环节是库存管理的关键部分,确保了库存的准确性。

  • 改派
    : 在履约过程中,可能会因为库存不足、配送问题或其他突发情况,需要重新分配订单到另一个履约中心。改派环节允许订单根据实际状况进行动态调整。这个过程有助于优化资源利用,确保订单能够快速和准确地被履约。

  • 拣货
    : 是指根据订单信息从库存中挑选出具体商品的过程。这个环节要求高度的准确性和效率,错误拣选会直接影响到客户满意度。仓库工作人员通常会使用手持终端设备,确保按照订单要求准确无误地选择商品。

  • 打包
    : 拣货完成后,商品会被包装好,确保在运输过程中的安全和完整。打包过程还包括贴上运输标签和必要的配送信息,为商品的顺利配送做好准备。

  • 出库
    : 打包好的商品接下来会被记录为出库状态,这意味着商品正式发货出库,离开门店/仓库。

  • 物流配送
    : 商品出库后,将通过快递或同城配进行配送。这一阶段,物流公司或配送公司负责将商品从门店/仓库运输到客户指定的收货地址。物流配送环节的效率和可靠性直接影响到交付时间和客户满意度,需要精心的规划和执行。

  • 确认收货
    : 最后,当客户收到商品并确认无误后,订单履约流程完结。客户确认收货通常在线上平台上确认来完成。这一环节也是记录履约时效、收集客户反馈和提升服务质量的重要机会。

写在最后

本文详细解释了什么是订单履约系统及其重要性。

订单履约系统用来管理从接到销售订单,到把货品送到客户手中的整个业务过程,它是上游交易和下游仓储配送之间的桥梁。

系统的主要流程包括接收订单、占用库存、拣货、打包、运输以及交付等环节。通过技术手段,让订单处理过程更高效、更透明,从而提升客户体验。本文还深入讨论了业务流程的各个环节,包括接收订单、订单拆单、派单、预占库存、改派、拣货、打包、出库、物流配送和确认收货,每一步都必须无缝衔接,确保整个流程高效运转。