CRN论文阅读笔记

130 阅读4分钟

背景

笔者最近阅读了多篇优化广告多触点归因的论文,其中有两篇论文均使用了CRN(Counterfactual Recurrent Network)。在广告投放场景下,广告主一般会在多个渠道(媒体或广告平台)分配预算,进行多渠道的广告投放,而用户可能会在上述渠道流量上先后浏览到该游戏广告,直至最后发生转化行为。广告对用户的曝光被称为触点,某个广告对某个用户在多个渠道流量的多次曝光构成触点序列,被称为转化路径。广告主需要分析每个触点对最终转化的归因权重,以优化后续在不同渠道上的预算分配。多触点归因是一个典型的序列建模问题,而CRN一方面能通过RNN实现序列建模,另一方面能通过域对抗训练(Domain Adversarial Training)消除由Confounders带来的偏差。

图1 Confounder示例

这里以因果推断中一个经典的例子介绍一下什么是Confounder。通过统计发现,吃巧克力多的国家,也是获诺贝尔奖多的国家,是否可以因此得出因为吃巧克力多、所以获诺贝尔奖多的因果结论,显然这个结论是不符合常识、是错误的。而上述两个现象存在关联,可能是由于某个共同的原因,比如经济较发达,导致某个国家吃巧克力多,获诺贝尔奖也多,但是不能得出吃巧克力多和获诺贝尔奖多是因果关系。以上这种因为某种原因同时影响两个结果、并进而错误推断两个结果存在因果关系的问题被称为Confounder。

图2 广告投放场景下多触点归因的Confounder问题

而在广告投放场景下的多触点归因也存在Confounders问题。用户的静态特征和历史行为都会影响广告点击和最终转化,例如某男性数码爱好者点击了某款手机的广告并最终下单购买,并不能得出因为点击广告、所以下单购买的因果结论,其点击广告和下单购买可能都是因为其对该款手机感兴趣,即使没有广告曝光,也会主动搜索并购买。

图3 临床医疗的Confounder问题

本文是对CRN这篇论文的阅读笔记。该篇论文的应用场景是临床医疗领域。在临床医疗领域,医生对病人按时间分阶段实施治疗方案,在某个阶段,医生根据病人历史给出适当的治疗方案,并在下阶段观察治疗结果。论文希望基于大量的临床医疗数据(例如电子病历数据)进行模型训练,能够在某个阶段根据病人历史和治疗方案由模型预测治疗结果以决策最佳方案。病人历史、治疗方案和治疗结果按时间分多个阶段,这也是一个典型的序列建模问题。论文在CRN中基于表征学习思想,设计了序列到序列的架构,在编码器部分,通过RNN对过去的病人历史、治疗方案和治疗结果进行序列建模,并引入域对抗训练,最终输出病人历史的无偏表征,消除Confounder问题带来的偏差,在解码器部分,将编码器输出的病人历史无偏表征作为初始状态输入,通过RNN对未来某治疗方案序列下的治疗结果进行预测,并更新病人历史无偏表征。

问题建模

令数据集D={{xt(i),at(i),yt+1(i)}t=1T(i){v(i)}}i=1N\mathcal{D}=\left\{\{\text{x}_t^{(i)},\text{a}_t^{(i)},\text{y}_{t+1}^{(i)}\}_{t=1}^{T^{(i)}}\cup\{\text{v}^{(i)}\}\right\}_{i=1}^N,其中包含NN个病人的数据,对于病人ii,其数据包含两部分,一部分是病人静态特征V(i)V\text{V}^{(i)}\in\mathcal{V},例如性别,另一部分是随时间变化的序列数据,分为T(i)T^{(i)}步,对于第tt步,其数据又分为病人随时间变化的特征Xt(i)Xt\text{X}_t^{(i)}\in\mathcal{X}_t,治疗方案At(i){A1,,AK}=A\text{A}_t^{(i)}\in\{A_1,\dots,A_K\}=\mathcal{A},以及在下一步产出的治疗结果Yt+1(i)Yt+1\text{Y}_{t+1}^{(i)}\in\mathcal{Y}_{t+1}。论文指出后面的分析会省略变量中的病人序号ii。 令Hˉt=(Xˉt,Aˉt1,V)\bar{\text{H}}_t=(\bar{\text{X}}_t,\bar{\text{A}}_{t-1},\text{V})表示病人在第tt步的历史,其中Xˉt=(X1,,Xt)\bar{\text{X}}_t=(\text{X}_1,\dots,\text{X}_t)表示截至第tt步病人随时间变化的特征序列,V\text{V}表示病人静态特征,Y[aˉ]\text{Y}[\bar{\text{a}}]表示未来采用治疗方案序列aˉ\bar{\text{a}}后可能的治疗结果。论文的目标是基于第tt步的病人历史,预测第tt步之后(包含第tt步)采用治疗方案序列aˉ\bar{\text{a}}后的治疗结果,可表示为:

E(Yt+τ[aˉ(t,t+τ1)]Hˉt)\mathbb{E}(\text{Y}_{t+\tau}[\={\text{a}}(t,t+\tau-1)]|\={\text{H}}_t)

其中aˉ(t,t+τ1)=[at,,at+τ1]\={\text{a}}(t,t+\tau-1)=[\text{a}_t,\dots,\text{a}_{t+\tau-1}]表示第tt步到第t+τ1t+\tau-1步的治疗方案序列,Yt+τ[aˉ(t,t+τ1)]\text{Y}_{t+\tau}[\={\text{a}}(t,t+\tau-1)]表示采用治疗方案序列aˉ\bar{\text{a}}后在第t+τt+\tau步产出的治疗结果。

网络结构

图4 CRN网络结构

CRN网络结构如图4所示,采用序列到序列的架构,分为编码器和解码器两部分,两者结构类似,但作用不同,并分别训练,在编码器部分,通过RNN对过去的病人历史、治疗方案和治疗结果进行序列建模,并引入域对抗训练,最终输出病人历史的无偏表征,消除Confounder问题带来的偏差,在解码器部分,将编码器输出的病人历史无偏表征作为初始状态输入,通过RNN对未来某治疗方案序列下的治疗结果进行预测,并更新病人历史无偏表征。 编码器层是一个典型的RNN/LSTM网络结构,第tt步的输入包含上一步的隐状态ht1h_{t-1}、上一步的治疗方案At1\text{A}_{t-1}、病人的静态特征V\text{V}和随时间变化的特征Xt\text{X}_t,第tt步的输出为当前步的隐状态hth_t,第tt步的输出再输入到一个全连接网络(激活函数采用ELU函数)中,由全连接网络输出第tt步病人历史的无偏表征Φ(Hˉt)\Phi(\bar{\text{H}}_t)。CRN的代码开源,代码地址为:github.com/ioanabica/C…。上述通过RNN最终输出病人历史无偏表征的代码如下所示:

def build_balancing_representation(self):
    self.rnn_input = tf.concat([self.current_covariates, self.previous_treatments], axis=-1)
    self.sequence_length = self.compute_sequence_length(self.rnn_input)

    #采用LSTM
    rnn_cell = DropoutWrapper(LSTMCell(self.rnn_hidden_units, state_is_tuple=False),
                              output_keep_prob=self.rnn_keep_prob,
                              state_keep_prob=self.rnn_keep_prob,
                              variational_recurrent=True,
                              dtype=tf.float32)

    decoder_init_state = None
    if (self.b_train_decoder):
        decoder_init_state = tf.concat([self.init_state, self.init_state], axis=-1)

    rnn_output, _ = rnn.dynamic_rnn(
        rnn_cell,
        self.rnn_input,
        initial_state=decoder_init_state,
        dtype=tf.float32,
        sequence_length=self.sequence_length)

    # Flatten to apply same weights to all time steps.
    rnn_output = tf.reshape(rnn_output, [-1, self.rnn_hidden_units])
    # 将RNN的输出再输入到全连接网络(激活函数采用ELU函数)中,由全连接网络输出病人历史的无偏表征
    balancing_representation = tf.layers.dense(rnn_output, self.br_size, activation=tf.nn.elu)

Φ(Hˉt)\Phi(\bar{\text{H}}_t)将病人历史Hˉt\bar{\text{H}}_t映射至表征空间R\mathcal{R},映射后的表征需要满足以下要求:

P(Φ(Hˉt)At=A1)==P(Φ(Hˉt)At=AK)P(\Phi(\bar{\text{H}}_t)|\text{A}_t=A_1)=\cdots=P(\Phi(\bar{\text{H}}_t)|\text{A}_t=A_K)

也就是说,不管采用哪种治疗方案,其病人历史表征的概率分布是相同的,消除了病人历史和当前治疗方案的关联,从而也就消除了Confounder问题带来的偏差。 具体如何实现上述要求,论文将不同治疗方案看做迁移学习的不同域,使用域适应(Domain Adaption)中的域对抗训练(Domain Adversarial Training)方法。CRN分别设计了治疗方案预测网络Ga(Φ(Hˉt);θa)G_a(\Phi(\bar{\text{H}}_t);\theta_a)θa\theta_a表示网络参数)和治疗结果预测网络Gy(Φ(Hˉt);θy)G_y(\Phi(\bar{\text{H}}_t);\theta_y)θy\theta_y表示网络参数)。将GaG_a作为域对抗训练中的域分类器,将GyG_y作为域对抗训练中的结果预测器。GaG_a的输入是病人历史表征,通过Softmax函数输出各种治疗方案的概率,代码如下所示:

    def build_treatment_assignments_one_hot(self, balancing_representation):
        balancing_representation_gr = flip_gradient(balancing_representation, self.alpha)

        treatments_network_layer = tf.layers.dense(balancing_representation_gr, self.fc_hidden_units,
                                                   activation=tf.nn.elu)
        treatment_logit_predictions = tf.layers.dense(treatments_network_layer, self.num_treatments, activation=None)
        treatment_prob_predictions = tf.nn.softmax(treatment_logit_predictions)

        return treatment_prob_predictions

GyG_y的输入是病人历史表征和当前治疗方案,通过线性函数输出在下一步产出的治疗结果,代码如下所示:

    def build_outcomes(self, balancing_representation,):
        current_treatments_reshape = tf.reshape(self.current_treatments, [-1, self.num_treatments])

        outcome_network_input = tf.concat([balancing_representation, current_treatments_reshape], axis=-1)
        outcome_network_layer = tf.layers.dense(outcome_network_input, self.fc_hidden_units,
                                                    activation=tf.nn.elu)
        outcome_predictions = tf.layers.dense(outcome_network_layer, self.num_outputs, activation=None)

        return outcome_predictions

GaG_a是一个分类问题,损失函数采用交叉熵损失函数,公式如下所示:

Lt,a(i)(θr,θa)=j=1KI{at(i)=aj}log(Gaj(Φ(Hˉt;θr);θa))\mathcal{L}_{t,a}^{(i)}(\theta_r,\theta_a)=-\sum_{j=1}^{K}{\mathbb{I}_{\{a_t^{(i)}=a_j\}}\log(G_a^j(\Phi(\bar{\text{H}}_t;\theta_r);\theta_a))}

代码如下:

    def compute_loss_treatments_one_hot(self, target_treatments, treatment_predictions, active_entries):
        treatment_predictions = tf.reshape(treatment_predictions, [-1, self.max_sequence_length, self.num_treatments])
        cross_entropy_loss = tf.reduce_sum(
            (- target_treatments * tf.log(treatment_predictions + 1e-8)) * active_entries) \
                             / tf.reduce_sum(active_entries)
        return cross_entropy_loss

模型训练时,并不是希望治疗方案预测的损失函数越小越好,而是越大越好,即病人历史表征无法预测治疗方案,这样能够消除病人历史和当前治疗方案的关联,从而也就消除了Confounder问题带来的偏差。 GyG_y是一个回归问题,损失函数采用MSE,公式如下所示:

Lt,y(i)(θr,θy)=Yt+1(i)(Gy(Φ(Hˉt;θr),θy))2\mathcal{L}_{t,y}^{(i)}(\theta_r,\theta_y)=\parallel\text{Y}_{t+1}^{(i)}-(G_y(\Phi(\bar{\text{H}}_t;\theta_r),\theta_y))\parallel^2

代码如下:

    def compute_loss_predictions(self, outputs, predictions, active_entries):
        predictions = tf.reshape(predictions, [-1, self.max_sequence_length, self.num_outputs])
        mse_loss = tf.reduce_sum(tf.square(outputs - predictions) * active_entries) \
                   / tf.reduce_sum(active_entries)

        return mse_loss

模型训练时,希望治疗结果预测的损失函数越小。将GaG_aGyG_y的损失函数整合后的公式如下所示:

Lt(θr,θy,θa)=i=1NLt,y(i)(θr,θy)λLt,a(i)(θr,θa)\mathcal{L}_t(\theta_r,\theta_y,\theta_a)=\sum_{i=1}^N{\mathcal{L}_{t,y}^{(i)}(\theta_r,\theta_y)-\lambda\mathcal{L}_{t,a}^{(i)}(\theta_r,\theta_a)}

最小化上述损失函数,即最小化治疗结果预测的损失函数,最大化治疗方案预测的损失函数,其中λ\lambda作为超参,用于权衡病人历史表征的无偏性和治疗结果预测的准确性。 训练时的前向、反向传播如图5所示。前向传播分别计算治疗方案预测和治疗结果预测的损失函数La\mathcal{L}_aLy\mathcal{L}_y,根据Ly\mathcal{L}_y反向传播更新模型参数时,分别计算GyG_y和病人历史表征(RNN+全连接神经网络)的梯度Lyθy\frac{\partial\mathcal{L}_y}{\partial\theta_y}Lyθr\frac{\partial\mathcal{L}_y}{\partial\theta_r},并按梯度更新参数,根据La\mathcal{L}_a反向传播更新模型参数时,分别计算GaG_a和病人历史表征(RNN+全连接神经网络)的梯度Laθa\frac{\partial\mathcal{L}_a}{\partial\theta_a}Laθr\frac{\partial\mathcal{L}_a}{\partial\theta_r},对于GaG_a,按照λLaθa\lambda\frac{\partial\mathcal{L}_a}{\partial\theta_a}更新参数,即令其自身预测治疗方案尽量准确,但对于病人历史表征(RNN+全连接神经网络),采用梯度反转层(Gradient Reversal Layer,GRL),按照λLaθr-\lambda\frac{\partial\mathcal{L}_a}{\partial\theta_r}更新参数,即令病人历史表征预测治疗方案尽量错误。

图5 训练时的前向、反向传播

CRN的解码器结构与编码器基本类似,不同之处有:

  • 编码器输出的病人历史无偏表征作为解码器的初始状态输入;
  • 解码器推理时,可设计多种可能的治疗方案序列aˉ(t,t+τ1)\={\text{a}}(t,t+\tau-1)作为输入,由解码器预测每一步的治疗结果,并且每一步的治疗结果以自回归的方式用作下一步的输入,最后根据治疗结果决策最佳治疗方案。

模型训练

CRN模型训练伪代码如图6所示。

图6 模型训练伪代码

模型训练可粗略分为4步:

  • 分批次(每批包含多个病人的数据)对编码器进行训练,前向、反向传播细节已在网络结构中介绍;
  • 对于每个病人、每步使用编码器计算相应的病人历史无偏表征;
  • 将每个病人的历史按最长步长τmax\tau_{\text{max}}拆分多个子序列;
  • 分批次(每批包含多个病人的多个历史子序列)对解码器进行训练,训练细节和编码器类似,不同之处只是对于子序列的第一个节点,将其相应前一个节点的病人历史无偏表征作为隐状态的初始值。

参考文献