论文精读|Learned Primal-Dual:把原始–对偶算法展开成网络

上一篇的 FBPConvNet 先做 FBP,再让 U-Net 去条纹。这条路简单好用,但有一个绕不开的上限:网络只看得到 FBP 图像,FBP 里已经丢掉的信息,后处理补不回来。

2017 年,KTH 的 Jonas Adler 和 Ozan Öktem 换了个思路:不先做 FBP,直接从原始投影数据出发,把一个经典的原始--对偶优化算法展开成 10 次迭代,每一次迭代里的近端算子换成小 CNN,正投影和反投影则原样留在网络里。这就是 Learned Primal-Dual(下面简称 LPD)。它和 FBPConvNet 一样,是深度学习 CT 重建论文里常被拿来对比的经典方法。

这篇按"它从哪个算法来 → 怎么改成网络 → 代码怎么写 → 实验说明了什么 → 读的时候要注意什么"的顺序讲。文中代码全部原样摘自作者开源仓库,数字全部来自论文原文。

  • 论文:J. Adler, O. Öktem. Learned Primal-Dual Reconstruction. IEEE Transactions on Medical Imaging, 2018, 37(6): 1322--1332(arXiv:1707.06474)
  • 代码:https://github.com/adler-j/learned_primal_dual(TensorFlow 1.x + ODL,算子用 ASTRA 的 GPU 后端)

一、出发点:PDHG(Chambolle--Pock)算法

(二)里讲过,带正则的重建可以写成

min ⁡ f    L ( T ( f ) , g ) + λ S ( f ) \min_{f} \; \mathcal{L}\big(T(f), g\big) + \lambda \mathcal{S}(f) fminL(T(f),g)+λS(f)

T T T 是前向算子(CT 里就是 Radon 变换 P P P), g g g 是测量数据。TV 这类正则不可导,常用近端(proximal)方法来解。论文选的是原始--对偶混合梯度法(PDHG),也就是 Chambolle--Pock 算法。它求解的是这样一类问题:

min ⁡ f    F ( K ( f ) ) + G ( f ) \min_{f} \; F\big(K(f)\big) + G(f) fminF(K(f))+G(f)

除了图像(原始变量) f f f,它还引入一个和数据同空间的对偶变量 h h h,两者交替更新。论文给出的(支持非线性 K K K 的)PDHG 每一步是:

h i + 1 = prox ⁡ σ F ∗ ( h i + σ K ( f ˉ i ) ) f i + 1 = prox ⁡ τ G ( f i − τ   ∂ K ( f i ) ∗ ( h i + 1 ) ) f ˉ i + 1 = f i + 1 + γ   ( f i + 1 − f i ) \begin{aligned} h_{i+1} &= \operatorname{prox}{\sigma F^*}\big(h_i + \sigma K(\bar f_i)\big) \\ f{i+1} &= \operatorname{prox}{\tau G}\big(f_i - \tau\, \\partial K(f_i)^*(h{i+1})\big) \\ \bar f_{i+1} &= f_{i+1} + \gamma\,(f_{i+1} - f_i) \end{aligned} hi+1fi+1fˉi+1=proxσF∗(hi+σK(fˉi))=proxτG(fi−τ∂K(fi)∗(hi+1))=fi+1+γ(fi+1−fi)

F ∗ F^* F∗ 是 F F F 的 Fenchel 共轭, ∂ K ( f ) ∗ \\partial K(f)^* ∂K(f)∗ 是 K K K 在 f f f 处导数的伴随。对线性的 Radon 变换,它就是反投影 P ∗ P^* P∗。以 TV 正则 CT 为例,论文取 K ( f ) = ( P f , ∇ f ) K(f) = (P f, \nabla f) K(f)=(Pf,∇f)、 F ( h ( 1 ) , h ( 2 ) ) = ∥ h ( 1 ) − g ∥ 2 2 + λ ∥ h ( 2 ) ∥ 1 F(h^{(1)}, h^{(2)}) = \lVert h^{(1)} - g \rVert_2^2 + \lambda \lVert h^{(2)} \rVert_1 F(h(1),h(2))=∥h(1)−g∥22+λ∥h(2)∥1、 G = 0 G = 0 G=0。

关键是结构:每一步 = 一次正投影 + 一个"数据空间里的近端算子" + 一次反投影 + 一个"图像空间里的近端算子"。

二、从 Learned PDHG 到 Learned Primal-Dual

1. 第一版:直接把近端算子换成网络(Learned PDHG)

前人已经发现,近端算子可以换成别的去噪器(比如 BM3D)。论文的第一步是:把两个 prox 换成可学习的网络 Γ θ d \Gamma_{\theta^d} Γθd(对偶)和 Λ θ p \Lambda_{\theta^p} Λθp(原始),步长 σ , τ \sigma, \tau σ,τ 和过松弛系数 γ \gamma γ 也一起学,迭代次数固定为 I I I。

结果并不好。作者原话的意思是:效果和传统方法相当,但没超过当时的深度学习方法。这一版在后面的表格里叫 Learned PDHG。

2. 四处修改

为了真正提升效果,作者做了四处修改:

  1. 给变量加"记忆" 。原始变量扩成 N primal N_{\text{primal}} Nprimal 个通道 f = f ( 1 ) , ... , f ( N primal ) \mathbf f = f\^{(1)}, \\dots, f\^{(N_{\\text{primal}})} f=f(1),...,f(Nprimal),对偶变量扩成 N dual N_{\text{dual}} Ndual 个通道,迭代之间可以传递更多信息;
  2. 不再硬编码 h i + σ K ( f ˉ i ) h_i + \sigma K(\bar f_i) hi+σK(fˉi),让网络自己学怎么把上一步的对偶变量和算子结果组合起来;
  3. 不再硬编码过松弛,让网络自己决定在哪个点上计算正投影;
  4. 每次迭代用不同的参数。参数量变多了,但重建质量明显提升。

得到的就是 Learned Primal-Dual:

h i = Γ θ i d ( h i − 1 ,    K ( f i − 1 ( 2 ) ) ,    g ) f i = Λ θ i p ( f i − 1 ,    ∂ K ( f i − 1 ( 1 ) ) ∗ ( h i ( 1 ) ) ) i = 1 , ... , I \begin{aligned} \mathbf h_i &= \Gamma_{\theta_i^d}\Big(\mathbf h_{i-1},\; K\big(f^{(2)}{i-1}\big),\; g\Big) \\ \mathbf f_i &= \Lambda{\theta_i^p}\Big(\mathbf f_{i-1},\; \big\\partial K\\big(f\^{(1)}_{i-1}\\big)\\big^*\big(h^{(1)}_i\big)\Big) \end{aligned} \qquad i = 1, \dots, I hifi=Γθid(hi−1,K(fi−1(2)),g)=Λθip(fi−1,∂K(fi−1(1))∗(hi(1)))i=1,...,I

最后输出 f I ( 1 ) f^{(1)}_I fI(1)。读这两行要抓住三个下标:

  • 正投影作用在原始变量的第 2 个通道 f ( 2 ) f^{(2)} f(2) 上(这就是"网络自己决定在哪个点算正投影");
  • 反投影作用在对偶变量的第 1 个通道 h ( 1 ) h^{(1)} h(1) 上;
  • 输出取原始变量的第 1 个通道 f ( 1 ) f^{(1)} f(1)。

初值论文用全零。作者也试过用 FBP 做初值,训练快了一点,但最终结果没有变好,所以论文只报告全零初值的结果。这意味着 LPD 完全不依赖 FBP,这是它和 FBPConvNet 最本质的区别。

3. 它能退化回 PDHG

论文指出,取 N primal = 2 N_{\text{primal}} = 2 Nprimal=2、 N dual = 1 N_{\text{dual}} = 1 Ndual=1,再把 Γ \Gamma Γ、 Λ \Lambda Λ 取成特定的形式,LPD 就恰好是经典 PDHG;梯度下降也能写成它的特例。由于神经网络的万能逼近性质,作者认为在训练充分时,LPD 在同样的迭代次数下应该至少不比这些变分方法差。

三、代码:网络到底长什么样

下面的代码都摘自作者仓库的 ellipses/learned_primal_dual.py(椭圆体模实验)和 human/mayo_learned_primal_dual.py(人体数据实验)。

1. 把 CT 算子变成网络层

python 复制代码
# Create ODL data structures
size = 128
space = odl.uniform_discr([-64, -64], [64, 64], [size, size],
                          dtype='float32')

geometry = odl.tomo.parallel_beam_geometry(space, num_angles=30)
operator = odl.tomo.RayTransform(space, geometry)

# Ensure operator has fixed operator norm for scale invariance
opnorm = odl.power_method_opnorm(operator)
operator = (1 / opnorm) * operator

# Create tensorflow layer from odl operator
odl_op_layer = odl.contrib.tensorflow.as_tensorflow_layer(operator,
                                                          'RayTransform')
odl_op_layer_adjoint = odl.contrib.tensorflow.as_tensorflow_layer(operator.adjoint,
                                                                  'RayTransformAdjoint')

几点说明:

  • 前向算子 operator 是 ODL 的 RayTransform: 128 × 128 128 \times 128 128×128 图像,30 个角度的平行束;
  • 用幂迭代估计算子范数 opnorm,再把算子缩放到范数为 1,注释写的目的是"尺度不变";
  • as_tensorflow_layer 把 ODL 算子和它的伴随(反投影)包装成 TensorFlow 层。这两层没有可学习参数,但能参与反向传播:训练时梯度要穿过全部 10 次正投影和反投影,算子部分的梯度由 ODL 提供。

2. 训练数据:随机椭圆 + 5% 高斯噪声

python 复制代码
    for i in range(n_generate):
        if validation:
            phantom = odl.phantom.shepp_logan(space, True)
        else:
            phantom = random_phantom(space)
        data = operator(phantom)
        noisy_data = data + odl.phantom.white_noise(operator.range) * np.mean(np.abs(data)) * 0.05

        x_true_arr[i, ..., 0] = phantom
        y_arr[i, ..., 0] = noisy_data

训练时每次随机生成椭圆体模,验证时用修改版 Shepp--Logan 体模。噪声是白噪声乘以"投影数据绝对值的均值 × 0.05",对应论文里的"5% 加性高斯噪声"。

3. 核心循环

python 复制代码
def apply_conv(x, filters=32):
    return tf.layers.conv2d(x, filters=filters, kernel_size=3, padding='SAME',
                            kernel_initializer=tf.contrib.layers.xavier_initializer())


with tf.name_scope('tomography'):
    with tf.name_scope('initial_values'):
        primal = tf.concat([tf.zeros_like(x_true)] * n_primal, axis=-1)
        dual = tf.concat([tf.zeros_like(y_rt)] * n_dual, axis=-1)

    for i in range(n_iter):
        with tf.variable_scope('dual_iterate_{}'.format(i)):
            evalop = odl_op_layer(primal[..., 1:2])
            update = tf.concat([dual, evalop, y_rt], axis=-1)

            update = prelu(apply_conv(update), name='prelu_1')
            update = prelu(apply_conv(update), name='prelu_2')
            update = apply_conv(update, filters=n_dual)
            dual = dual + update

        with tf.variable_scope('primal_iterate_{}'.format(i)):
            evalop = odl_op_layer_adjoint(dual[..., 0:1])
            update = tf.concat([primal, evalop], axis=-1)

            update = prelu(apply_conv(update), name='prelu_1')
            update = prelu(apply_conv(update), name='prelu_2')
            update = apply_conv(update, filters=n_primal)
            primal = primal + update

    x_result = primal[..., 0:1]

对照上面的公式读:

  • 初值 :原始变量 primal 是 5 个全零通道,对偶变量 dual 也是 5 个全零通道(n_primal = n_dual = 5),和论文一致;
  • 对偶更新 :primal[..., 1:2] 取原始变量第 2 个通道做正投影,和当前 dual、测量数据 y_rt 拼接,送进三层卷积,输出 5 个通道,再加回 dual;
  • 原始更新 :dual[..., 0:1] 取对偶变量第 1 个通道做反投影,和当前 primal 拼接,同样三层卷积后加回 primal;
  • 输出 :primal[..., 0:1],原始变量的第 1 个通道。

"加回去"就是论文里学习近端算子的形式

I d + W w 3 , b 3 ∘ A c 2 ∘ W w 2 , b 2 ∘ A c 1 ∘ W w 1 , b 1 \mathrm{Id} + \mathcal W_{w_3,b_3} \circ \mathcal A_{c_2} \circ \mathcal W_{w_2,b_2} \circ \mathcal A_{c_1} \circ \mathcal W_{w_1,b_1} Id+Ww3,b3∘Ac2∘Ww2,b2∘Ac1∘Ww1,b1

即一个残差块。作者给的理由是:近端算子本来就接近恒等映射,而且残差网络更容易训练。

从代码可以直接数出通道数:对偶网络输入 5(dual)+ 1(正投影)+ 1(数据)= 7 个通道,即 7→32→32→5;原始网络输入 5 + 1(反投影)= 6 个通道,即 6→32→32→5,都是 3 × 3 3 \times 3 3×3 卷积,和论文图 2 一致。每次迭代两个三层网络,10 次迭代共 60 个卷积层 ,但每层只有 32 个通道。按上面的代码逐层算(卷积权重 + 偏置 + PReLU 参数),总参数约 25.3 万,论文给的是"约 2.4 × 10 5 2.4 \times 10^5 2.4×105",量级一致。作为对比,论文里的 U-Net 后处理约 10 7 10^7 107 个参数。

激活函数 prelu 来自作者的工具库 adler(adler/tensorflow/activation.py):

python 复制代码
def prelu(_x, init=0.0, name='prelu', trainable=True):
    with tf.variable_scope(name):
        alphas = tf.get_variable('alphas',
                                 shape=[int(_x.get_shape()[-1])],
                                 initializer=tf.constant_initializer(init),
                                 dtype=tf.float32,
                                 trainable=trainable)
        pos = tf.nn.relu(_x)
        neg = -alphas * tf.nn.relu(-_x)

        return pos + neg

每个通道一个可学习的负半轴斜率 alphas,初值 init=0.0,所以训练开始时它就是 ReLU。

4. 损失和优化器

python 复制代码
with tf.name_scope('loss'):
    residual = x_result - x_true
    squared_error = residual ** 2
    loss = tf.reduce_mean(squared_error)


with tf.name_scope('optimizer'):
    # Learning rate
    global_step = tf.Variable(0, trainable=False)
    maximum_steps = 100001
    starter_learning_rate = 1e-3
    learning_rate = cosine_decay(starter_learning_rate,
                                 global_step,
                                 maximum_steps,
                                 name='learning_rate')

    update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
    with tf.control_dependencies(update_ops):
        opt_func = tf.train.AdamOptimizer(learning_rate=learning_rate,
                                          beta2=0.99)

        tvars = tf.trainable_variables()
        grads, _ = tf.clip_by_global_norm(tf.gradients(loss, tvars), 1)
        optimizer = opt_func.apply_gradients(zip(grads, tvars),
                                             global_step=global_step)
  • 损失是重建结果和真值的均方误差,端到端:误差从最后一次迭代一路反传到第一次迭代;
  • Adam,beta2 = 0.99,其他参数默认;
  • 全局梯度范数裁剪到 1;
  • 学习率从 10 − 3 10^{-3} 10−3 开始按余弦退火,共 maximum_steps = 100001 步。cosine_decay 同样来自 adler(adler/tensorflow/training.py):
python 复制代码
    p = tf.mod(global_step / maximum_steps, 1)

    return learning_rate * (0.5 + 0.5 * math_ops.cos(p * np.pi))

即 η t = η 0 ⋅ 1 2 ( 1 + cos ⁡ ( π t / t max ⁡ ) ) \eta_t = \eta_0 \cdot \tfrac12\big(1 + \cos(\pi t / t_{\max})\big) ηt=η0⋅21(1+cos(πt/tmax)),从 10 − 3 10^{-3} 10−3 平滑降到接近 0,和论文写的公式一致。

另外从训练循环可以看到:每次生成 n_data = 5 张随机体模(即 batch size 5),每 10 步才重新生成一批(if i%10 == 0)。论文明确说没用 BN、dropout,也没做数据增强,代码里同样没有。

5. 非线性前向模型:直接用"取对数之前"的数据

人体数据实验里,作者用了更符合物理的 Beer--Lambert 定律做前向模型:

T ( f ) = e − μ P ( f ) , ∂ T ( f ) ∗ ( g ) = P ∗ ( − μ   e − μ P ( f )   g ) T(f) = e^{-\mu P(f)}, \qquad \\partial T(f)^*(g) = P^*\big(-\mu\, e^{-\mu P(f)}\, g\big) T(f)=e−μP(f),∂T(f)∗(g)=P∗(−μe−μP(f)g)

网络直接吃未取对数的透射数据 。对应代码 human/mayo_learned_primal_dual.py:

python 复制代码
    for i in range(n_iter):
        with tf.variable_scope('dual_iterate_{}'.format(i)):
            evalpt = primal[..., 1:2]
            evalop = tf.exp(-mu_water * odl_op_layer(evalpt))
            update = tf.concat([dual, evalop, y_rt], axis=-1)

            update = prelu(apply_conv(update), name='prelu_1')
            update = prelu(apply_conv(update), name='prelu_2')
            update = apply_conv(update, filters=n_dual)
            dual = dual + update

        with tf.variable_scope('primal_iterate_{}'.format(i)):
            evalpt_fwd = primal[..., 0:1]
            evalop_fwd = (-mu_water) * tf.exp(-mu_water * odl_op_layer(evalpt_fwd))

            evalpt = dual[..., 0:1]
            evalop = odl_op_layer_adjoint(evalop_fwd * evalpt)
            update = tf.concat([primal, evalop], axis=-1)

            update = prelu(apply_conv(update), name='prelu_1')
            update = prelu(apply_conv(update), name='prelu_2')
            update = apply_conv(update, filters=n_primal)
            primal = primal + update

和椭圆版本相比只改了两处,正好对应上面两个公式:

  • 对偶更新里,正投影换成 tf.exp(-mu_water * odl_op_layer(evalpt)),即 T ( f ( 2 ) ) T(f^{(2)}) T(f(2));
  • 原始更新里,先在 primal[..., 0:1](即 f ( 1 ) f^{(1)} f(1))处算出 − μ e − μ P ( f ( 1 ) ) -\mu e^{-\mu P(f^{(1)})} −μe−μP(f(1)),乘上 h ( 1 ) h^{(1)} h(1) 再反投影,即 ∂ T ( f ( 1 ) ) ∗ ( h ( 1 ) ) \\partial T(f\^{(1)})^*(h^{(1)}) ∂T(f(1))∗(h(1))。

网络结构完全不用改,只换算子 ,这是 LPD 框架通用性的直接体现。论文里 μ \mu μ 取 0.2 cm²/g(约为水的值),代码里 mu_water = 0.02。代码的几何参数(源到旋转中心 500)对应论文的 500 mm,所以这里应是按毫米换算的结果(0.2 cm⁻¹ = 0.02 mm⁻¹)。

仓库里还有一个线性化版本 human/mayo_learned_primal_dual_log.py,先对数据取对数,再用普通 Radon 变换。它的数据生成多了一步把光子计数截断到不小于 1,避免对 0 取对数:

python 复制代码
        noisy_data = odl.phantom.poisson_noise(data * photons_per_pixel)
        noisy_data = np.maximum(noisy_data, 1) / photons_per_pixel

        log_noisy_data = np.log(noisy_data) * (-1 / mu_water) * (1 / opnorm)

四、实验

1. 两组数据

实验 图像 几何 前向模型 噪声
椭圆体模 128 × 128 128 \times 128 128×128,随机椭圆训练,Shepp--Logan 验证 平行束,30 个角度,182 个探测器单元 线性 Radon 变换 5% 加性高斯
人体数据 512 × 512 512 \times 512 512×512,Mayo 低剂量挑战赛 3 mm 层厚,9 人训练(2168 张),1 人评估 扇束,1000 个角度,1000 个探测器单元,源--轴、轴--探测器各 500 mm 非线性 Beer--Lambert(另有线性化版本) 泊松,每个探测器单元入射 10 4 10^4 104 个光子

注意这里的"低剂量":椭圆实验是稀疏角度 + 强噪声 ;人体实验是全角度(1000 个)+ 低光子数 。和上一篇 FBPConvNet 的设置(稀疏角度、无噪声、参考图是全角度 FBP)不一样,这里的参考图是体模真值。

对比方法:FBP(Hann 滤波)、TV(经典 PDHG 迭代 1000 次)、作者自己的 Learned Gradient、FBP + U-Net 后处理(即 FBPConvNet 的结构)、Learned PDHG,以及两个简化版:只学原始网络的 Learned Primal,和完全不用前向算子的 FBP + 残差去噪。FBP 的滤波带宽和 TV 的正则参数都按 PSNR 最大来选。

2. 指标怎么算

指标是 PSNR 和 SSIM。评估脚本用的是 skimage 的函数。椭圆实验(ellipses/evaluate_learned_primal_dual.py):

python 复制代码
print(ssim(primal_values_result[-1][0, ..., 0], x_true_arr_validate[0, ..., 0]))
print(psnr(primal_values_result[-1][0, ..., 0], x_true_arr_validate[0, ..., 0], dynamic_range=1))

人体实验(human/evaluate_mayo_learned_primal_dual.py):

python 复制代码
print(ssim(primal_values_result[-1][0, ..., 0], x_true_arr_validate[0, ..., 0]))
print(psnr(primal_values_result[-1][0, ..., 0], x_true_arr_validate[0, ..., 0], dynamic_range=np.max(x_true_arr_validate) - np.min(x_true_arr_validate)))

(四)里专门讲过 PSNR 依赖 data_range:这里椭圆实验固定为 1,人体实验取真值图像的最大值减最小值。引用这些数字和别的论文比较时,要先确认 data_range 的取法是否一致。

3. 椭圆体模结果

方法 PSNR (dB) SSIM 运行时间 (ms) 参数量
FBP 19.75 0.597 4 1
TV 28.06 0.929 5166 1
FBP + U-Net 去噪 29.20 0.944 9 10 7 10^7 107
FBP + 残差去噪 32.38 0.972 9 1.2 × 10 5 1.2 \times 10^5 1.2×105
Learned Gradient 32.29 0.981 56 1.2 × 10 4 1.2 \times 10^4 1.2×104
Learned PDHG 28.32 0.909 48 2.4 × 10 4 2.4 \times 10^4 2.4×104
Learned Primal 36.97 0.986 43 1.2 × 10 5 1.2 \times 10^5 1.2×105
Learned Primal-Dual 38.28 0.989 49 2.4 × 10 5 2.4 \times 10^5 2.4×105
  • 比 FBP、TV 和 U-Net 后处理都高出 6 dB 以上,这也是摘要里"> 6 dB"的来源;
  • 最接近的是 Learned Primal,差 1.3 dB。它和 LPD 的区别在于对偶变量不学习(n_dual = 1),直接取残差:
python 复制代码
        with tf.variable_scope('dual_iterate_{}'.format(i)):
            evalop = odl_op_layer(primal[..., 1:2])
            dual = evalop - y_rt

说明大部分增益来自"在网络里反复用前向算子",可学习的对偶网络再多给 1.3 dB;

  • Learned PDHG(只把 prox 换成网络)只有 28.32 dB,连 TV 都没明显超过,说明上面那四处修改是必要的;
  • 速度:带算子的学习方法比纯后处理慢约 6 倍,但都比 TV 快两个数量级以上。

4. 人体数据结果

方法 PSNR (dB) SSIM 运行时间 (ms) 参数量
FBP 33.65 0.830 423 1
TV 37.48 0.946 64371 1
FBP + U-Net 去噪 41.92 0.941 463 10 7 10^7 107
LPD,线性 44.11 0.969 620 2.4 × 10 5 2.4 \times 10^5 2.4×105
LPD,非线性 43.91 0.969 670 2.4 × 10 5 2.4 \times 10^5 2.4×105

(论文图 4 中线性版本标注为 44.10 dB,表 II 为 44.11 dB,这里按表格。)

  • LPD 比 FBP 高 10.5 dB,比 TV 高 6.6 dB,比 U-Net 后处理高 2.2 dB;
  • 一个有意思的现象:U-Net 后处理的 PSNR 比 TV 高 4.4 dB,但 SSIM 反而略低(0.941 vs 0.946);LPD 两项都明显更好。作者的解释是:后处理受限于 FBP 图像所含的信息,而 LPD 直接用数据,受限于数据所含的信息,后者不少于前者;
  • 线性和非线性版本结果几乎一样,线性版略好。作者据此认为,在取对数之后的数据上做重建可能更可取;
  • 512 × 512 512 \times 512 512×512 时 FBP 本身也要 423 ms,所以 LPD 的 620 ms 已经和 FBP、后处理处在同一量级。

5. 迭代过程的可视化

论文图 5 画了非线性版本第 2、4、6、8、10 次迭代的中间结果:前几次迭代的图像一直很差,到第 8 次时大部分结构才出现,但仍有噪声;第 8 到第 10 次像在做边缘增强。作者据此推测,网络学到的是"先找大尺度结构,再精修细节"的两阶段过程。

五、读这篇论文时要注意的几点

  1. 评估用的图很少。 椭圆实验的验证图只有一张 Shepp--Logan;人体实验的评估脚本也只对留出病人(L286)的一张固定切片计算指标。论文表格没有给出多张测试图的平均值或方差。
  2. U-Net 对比是作者重新实现的。 论文说按 FBPConvNet 原文的训练方式效果不理想,所以改用和自己方法相同的训练流程。这是为了公平,但也意味着表格里的 U-Net 结果不等于原作者的实现。
  3. 几何和噪声水平是固定的。 前向算子在代码开头就定死了(30 角平行束、1000 角扇束),换几何就要重新训练;训练时梯度要穿过 10 次正投影和反投影,人体数据的训练用了约 40 小时(GTX 1080 Ti),椭圆数据约 11 小时。
  4. 结果偏平滑。 作者在讨论里承认,重建图在视觉上有过度平滑,猜测主要原因是均方误差损失,建议后续尝试感知损失之类的更好的损失函数。
  5. 环境比较老。 代码基于 TensorFlow 1.x(tf.placeholder、tf.contrib)和当时 ODL 的开发版本,直接跑需要旧环境;README 写的是预训练模型"即将发布"。

六、它为什么重要

把 LPD 和上一篇的 FBPConvNet 放在一起看,区别很清楚:

FBPConvNet Learned Primal-Dual
输入 FBP 图像 原始投影数据
物理模型 只在网络之前用一次 每次迭代都用一次正投影和反投影
网络 一个大 U-Net(约 10 7 10^7 107 参数) 20 个小残差 CNN(约 2.4 × 10 5 2.4 \times 10^5 2.4×105 参数)
上限 受 FBP 所含信息限制 受数据所含信息限制

LPD 证明了:把前向算子嵌进网络、端到端训练,在 512 × 512 512 \times 512 512×512 的临床尺寸上是可行的,而且参数少得多。之后不少展开网络、双域网络,都采用了类似的"数据域网络 + 图像域网络,用正投影和反投影连接"的结构。

这些方向的代表论文、开源代码和数据集,我整理在了一个开源列表里,可以按方向筛选、支持中文搜索:

参考文献

  1. Adler J, Öktem O. Learned primal-dual reconstruction. IEEE Transactions on Medical Imaging, 2018, 37(6): 1322--1332.
  2. Chambolle A, Pock T. A first-order primal-dual algorithm for convex problems with applications to imaging. Journal of Mathematical Imaging and Vision, 2011, 40(1): 120--145.

LPD 的代码里,正投影、反投影都是 ODL 调 ASTRA 完成的。下一篇回到基础系列,讲 CT 重建里常用的投影算子库:ASTRA、ODL、TIGRE 等各自能做什么,怎么选。

如果这篇对你有帮助,欢迎收藏,有问题可以在评论区讨论。

相关推荐
yuniko-n1 小时前
【算法】约瑟夫环问题
java·算法
李航19831 小时前
DeepSeekAI建模三 - 实战建模一组柜子
人工智能·python·计算机视觉·ai·ai编程
l1t1 小时前
测试DuckDB v2.1.0-alpha49002的bignum乘法
数据库·sql·算法·duckdb
SteveSenna1 小时前
RealSense D435i使用指南
计算机视觉
Nil2081 小时前
LeetCode 283移动零
数据结构·算法·leetcode
CQU_JIAKE2 小时前
10.18【A】
算法
LMT流氓兔2 小时前
第五单元——第4课:给工具参数加校验
java·前端·算法·ai·langchain
原子延迟2 小时前
视频太大怎么压缩发微信?先查限制再压的3种方法(2026)
开发语言·图像处理·人工智能·计算机视觉·音视频
物联网软硬件开发-轨物科技3 小时前
【轨物方案】深度解读开关柜智能感知及一键顺控专家系统
大数据·运维·网络·人工智能·算法·机器人