KD+SM Uplift Modeling. Часть 2. T-Learner. Субпопуляции и Дивергенции

В предыдущей (вводной) статье мы дали определения и математическое описание базовым понятиям связанным с Uplift моделированием. Рассмотрели принцип работы популярных Uplift моделей и их недостатки, а так же установили, как мы рассчитываем итоговый инкремент \tau, дали определения контрольной Cи тестовой T группам. А так же условились, что же такое воздействиеf на пользователя u_{i}.

Среди проблем с которыми сталкиваются современные Uplif модели, пожалуй, самая серьезная — counterfactual sample pairs problem, рассмотренная так же в предыдущей статье.

Данная статься посвящена как раз нивелированию данной проблемы. Так как мы используем архитектуру KDSM Uplift modeling, то первые две буквы аббревиатуры (KD — Knowledge Distillation) представляют собой название модели, задача которой является создание подмножеств генерального множества, таким образом чтобы минимизировать или же по крайней мери свести к минимуму влияния counterfactual sample pairs problem на результат и точность итоговой модели. Само по себе слово Distillation намекает, что мы будем стремиться стратифицировать множество таким образом, чтобы можно было найти «похожих» друг на друга пользователей u_{i} и u_{j}из C (W=0) и T (W=1) соответственно, объединить их в одно подмножество, чтобы в дальнейшем можно было сделать допущение, что u_{i}и u_{j}представляют из себя уже единого синтетического пользователя u_{k}. Где пользователь u_{i} вместе с его параметрами X_{i}, Y_{i} выполняет роль пользователяu_{k}, с которым мы не взаимодействовали W=0, а пользователь u_{j} вместе с его параметрами X_{j}, Y_{j} выполняет роль пользователя u_{k}, с которым мы провзаимодействовали W=1.

\begin{cases}       u_{k}|(x_{k},y_{k},W=0) =u_{i}|(x_{i},y_{i}W=0) \\ u_{k}|(x_{k},y_{k},W=1) =u_{j}|(x_{j},y_{j},W=1)         \end{cases}

Таким образом мы сможем собрать подмножества, которые будут являться обучающими датасетами для будущих моделей прямого расчета Uplift. По сути, в этой статье нам предстоит описать алгоритм, который только лишь соберет корректные данные (Distilled datasets) для будущего обучения. Правильно собранные и хорошо подготовленные данные — это уже половина успешно выполненной задачи, именно поэтому мы столько сил и внимания уделим этой проблеме, которую очень часто игнорируют. В архитектуре KD+SM этот алгоритм называется Teacher Learner или же просто T-Learner модель. Далее мы уже более строго опишем как он работает и приведем примеры кода. А пока, мы введем еще несколько новых сущностей, которые упростят нам понимание работы T-Learner модели.

Subpopulation

Но, что же вообще значит для нас «найти похожих друг на друга пользователей»?

Как уже ранее обсуждалось, итоговый инкремент представляет собой математическое ожидание индивидуальных (частных) инкрементов каждого из пользователей множества.

\tau = E[\tau_{i}]

Пришло время ввести промежуточную сущность между пользователем u_{i}и конечным множеством пользователей U. Субпопуляция subили же как более принято в маркетинге, говоря простым языком — пользовательская аудитория. Субпопуляция — это группа пользователей обладающих общим набором параметров (фичей) и имеющих схожие паттерны поведения. Говоря, про общие параметры, мы имеем ввиду набор фичей X=x пользователя u_{i}, а под паттерном поведения понимаем вероятностное распределение target переменной Y функции воздействия f или же кратко, отклик пользователя. Примером отклика может служить высокий CTR рекламного баннера среди пользователей (положительный отклик) или же непродолжительное время на странице текущей вариации A/B теста (негативный отклик). Пользователи (множество пользователей), которые имеют схожий набор параметровX=x называются активной аудиторией (active audience) и обозначается она как \alpha_{a}. Это те параметры, на основе которых мы выделяем группы пользователей , основываясь на своем жизненном или бизнес опыте, например, мужчины старше 45 лет и проживающие в городе Москве. Таким образом активная аудитория\alpha_{a}, как правило имеет эвристическую природу, основанную на субъективном взгляде человека, который ее создает и обозначается как:

\Gamma(X_{i},X_{j})

где \Gamma— мера схожести пользователей, базирующуюся исключительно на их параметрах.

Такой эвристический подход полагает, что если пользователи обладают неким набором схожих параметров, то и отклик на внешнее воздействие f(u_{i}) у них так же должен быть схож. Отчасти это действительно так, но только с очень большими допущения, так как среди тех же мужчин старше 45 лет из Москвы есть автолюбители, рыбаки, любители футбола или шахмат. То есть довольно неоднородный интерес среди казалось бы однородной аудитории. И тут , говоря уже про «интерес», мы приближаемся уже к другому механизму формирования аудитории.

Пользователи, имеющие же схожее вероятностное распределение target переменной Y в результате воздействия f(отклик), называются в свою очередь реактивной аудиторией (reactive audience) \alpha_{r}. Группировка пользователей в аудиторию в этом случае игнорирует их параметры X =x, а берет во внимание лишь вероятные распределения положительных и отрицательных откликов на внешнее воздействие. Схожесть же между вероятностными распределениями переменной Y двух пользователей u_{i}и u_{j}, обозначим как:

D(f(u_{i}),f(u_{j}))

где D является мерой расстояния между вероятностными распределениями о которой мы поговорим чуть позже. То есть, чтобы сформировать реактивную аудиторию \alpha_{r} мы берем во внимание лишь то как пользователи реагируют на воздействие f, игнорируя при этом их параметры X=x, например, любить бокс может как и пожилой директор мебельной фирмы, так и студентка первого курса. Оба этих человека будут принадлежать одной и той же реактивной аудитории \alpha_{r}.

Таким образом, субпопуляция sub представляет собой одновременно выполнение условий формирования как активной, так и реактивной аудитории. Мы стремимся собрать пользователей схожих не только своими неотъемлемыми параметрами X=x, но так же и схоже реагирующих (откликающихся) на воздействие f(u_{i}).

В итоге субпопуляцию можно записать как композицию \GammaиD:

sub = \begin{cases}       \Gamma(u_{i},u_{j}) \circ  D(f(u_{i}),f(u_{j}) &  \\\   D(f(u_{i}),f(u_{j})) \rightarrow  \text {max}\ \\ \Gamma(u_{i},u_{j}) \rightarrow    {max}      \end{cases}

Пользователь u_{i} может принадлежать одной и только лишь одной субпопуляции sub_{p}. Если исходное множество Uразбито на p популяций sub, то конъюнкция множеств sub_{p} образует множество U. Таким образом, пользователи образуют субпопуляции, а субпопуляции в свою очередь представляют образуют конечное множество пользователей U.

\begin{cases} sub_{p}\subseteq U\\      \ U = \cup_{p}sub_{p}\\      \end{cases}

Тогда частный инкремент субпопуляции \tau_{sub_{p}} — это ни что иное как математическое ожидание частных инкрементов пользователей u_{i}, принадлежащих субпопуляции sub_{p}.

\begin{cases}      \ sub_{p} = \Gamma(u_{i}) \circ D(u_{i}) & \text {subpopulation}\\ \tau_{sub_{p}} = E[\tau_{i}^{sub_{p}}]  & \text {subpop. uplift}\\      \end{cases}

Теперь мы можем модифицировать уравнение конечного инкремента \tau. Конечный инкремент \tau множества пользователей U представляет собой математическое ожидание частных инкрементов субпопуляций \tau_{i}^{sub}.

\tau = E[\tau_{i}^{sub_{p}}]

Разные субпопуляции sub_{p} будут нести разный инкремент (положительный, либо отрицательный), у одной субпопуляции Uplift будет выше чем у другой, а уже математическое ожидание инкрементов этих самых субпопуляций даст нам итоговый инкремент. Благодаря вводу такого понятия как субпопуляция, мы добьемся персонализированного расчета Uplift, что позволит гораздо более корректно и самое главное точно оценивать наши взаимодействия с пользователями, выйти за рамки ретроспективных расчетов Uplift и прогнозировать его в будущем для каждого пользователя, субпопуляции или же всех пользователей сразу.

Subpopulation. Example

Давайте уже наконец перейдем к примеру, чтобы продемонстрировать зачем же нам все таки нужны субпопуляции, чтобы рассчитать конечный инкремент. Представим, что мы проводим рекламную кампанию мобильного приложения, которое занимается онлайн стримингом гонок Формулы-1 (F1). Пусть у нас есть некий бенчмарк по установкам приложения CR (conversion rate), назовем его контрольным бенчмарком. И вот мы решаемся создать новый рекламный баннер (тестовый креатив), куда поместим один из болидов команды F1, Aston Martyn AMR24, а затем замерим как изменится % скачиваний по сравнению с бенчмарком и проверим работает ли тестовый креатив хуже или лучше. Мы сейчас рассуждаем об очень «стерильных» условиях эксперимента, на практике так конечно же никогда не происходит, но нам сейчас этого и не требуется, чтобы понять смысл субпопуляции. Для ясности эксперимента, допустим, что с баннером контактировали только три аудитории (субпопуляции) :

  • Субпопуляция Аsub^{A}. Составляет 55% от всех пользователей, которые увидели тестовый баннер, где CR (conversion rate) по сравнению с бенчмарком — сильно отрицательный, и составляет (-2.3%).

  • Субпопуляция Вsub^{B}. Составляет 35% от всех пользователей, которые увидели тестовый баннер, где CR (conversion rate) по сравнению с бенчмарком — положительный, но колеблется около нуля (+0.5%).

  • Субпопуляция Csub^{C}. Составляет всего 10% от всех пользователей, которые увидели тестовый баннер, где CR (conversion rate) по сравнению с бенчмарком — сильно положительный, и равен (+2.5%).

Тестовый креатив

Тестовый креатив

Какие выводы мы можем сделать из этих данных, если бы не стремились стратифицировать нашу выборку на субпопуляцию, а рассматривали всю картину целиком. Ну например, что тестовый креатив не работает, потому что самая большая субпопуляция A хуже всех устанавливает приложения (-2.3%) по сравнению с контрольным баннером, и как следствие, средний CR этого баннера оказался бы ниже показателя бенчмарка.

CR_{avg} = \sum_{i=0}(w_{i}*cr_{i})

где, w_{i} — доля субпопуляции, а cr_{i} — усредненный CR данной субпопуляции.

Вывод : тестовый баннер надо перестать откручивать и залить какой-нибудь другой.

Теперь взглянем на эту же картину, только уже со стороны субпопуляций. Да, sub^{A} действительно самая крупная из субпопуляций и действительно почему-то хуже всех устанавливает мобильное приложение F1. Это могут быть ярые фанаты других команд-конкурентов, например, Ferrari, McLaren или Red Bull Racing. Или же, например, люди которым не нравится зеленый цвет или сочетание цветов как на баннере. Причины нас на самом деле сейчас вообще не беспокоят, нас интересуют две оставшиеся субпопуляции sub^{B}и sub^{С}, которые показали положительный ростCR по сравнению с контрольный баннером. И если про успехsub^{B}(+0.5%) сложно сказать, что это однозначный успех, так как положительный рост в районе статистической ошибки, то про sub^{С} (+2.5%) мы можем однозначно сказать, что тестовый баннер нашел успех у данной аудитории и в дальнейшем sub^{С} следует работать именно этим баннером и никаким другим. Но так как sub^{С}самая маленькая из всех субпопуляций, на общую картину она никак не может повлиять, что не является для нас оправданием не работать с этой аудиторией персонально и не регистрировать ее довольно высокий CR. Субпопуляция sub^{С}может хоть и маленькая, но вполне может быть очень платежеспособной аудиторией, игнорирование которой принесло бы упущенную финансовую выгоду для владельцев приложения.

Но насколько же противоречивы эти два вывода. Первый вывод бракует тестовый баннер, в то время как второй утверждает, что тестовый баннер отличный, но отличный он не для всех, а лишь для определенной группы пользователей.

Итак, тезисы, которые несут в себе субпопуляции следующие:

  • Не бывает плохого креатива/рекламы/вариации и т.д. , а есть аудитория (субпопуляция) для которой они плохо работаю

  • Если что-то работает плохо, то скорее всего это что-то используют либо не там где надо, либо не для того кому это интересно.

A-test & Divergence

Вернемся к нашей задаче. Мы имеем пользователей принадлежащих либо treatment group T, либо к control groupC. Наша задача создать из этих пользователей конечное количество наиболее оптимальных субпопуляций. Для этого требуется строго определить на основе каких критериев мы будем рассчитывать active auditory \alpha_{a} и reactive auditory \alpha_{r}. Если быть точнее, мы опишем какими способами мы будет рассчитывать\Gammaдля \alpha_{a}иDдля \alpha_{r}, а затем переложим это на код.

  • active auditory

Для того, чтобы мы могли рассчитывать active auditory, введем понятие тестаA=a. Для каждой фичи X=x_{i} пользователя u_{i}, мы можем создать такой тест X \lt a, который разбивает множество на два подмножества:

A = \sum_{a}

Таким образом выполнив множество последовательных операций разбиения X \lt a, мы снижаем дисперсию в метриках пользователей и сокращаем разреженность. Таким образом, после каждого предиката X \lt a, мы увеличиваем значение близость\Gamma и однородность \alpha_{a}.

Условную вероятность распределения target переменной Yпользователей treatment group T, по результатам теста A=a, будем обозначать как P^{T}(Y|a). Аналогично P^{C} (Y|a) будет и для control group C. Такого рода разбиение всем знакомый механизм, который используется в Decision Tree и Random Forest. Есть одно но, в Random Forest любое разбиение оценивается выбранной заранее мерой качества этого самого разбиения (gini, entropy, etc.). Нам же эти критерии разбиения не подходят, так как наша задача в данный момент не состоит в том, чтобы правильно научиться различать купивших или не купивших пользователей, а в создании оптимальных субпопуляций.

  • reactive auditory

Ранее мы упомянули о мере схожестиD между вероятностными распределениями переменной Y, но не определились как ее считать. Мы имеет пользователейC и T, т.е. тех кто не контактировал с неким креативом или вариацией и тех кто контактировал. Если воздействие имело сильный эффект и действительно побуждало совершить покупку или увеличить сумму чека, то вероятностное распределение target переменной P^{T}(Y) будет отличаться от вероятностного распределения target переменной P^{С}(Y). Тут то как раз и появляется дивергенция (divergence). Мы не будем подробно останавливаться и расписывать что такое дивергенция и каков ее смысл, в угоду того чтобы не разрасталась статья, а лишь опишем наиболее важные моменты , которые нам понадобятся и стоит знать, чтобы понимать дальнейшее изложение. Первое, что стоит отметить, дивергенция это мера, которая показывает насколько друг от друга удалены вероятностные распределения переменных. Прочитать про дивергенцию можно по ссылку здесь.

Итак, наша цель максимизировать расстояние (разницу) D между вероятностными распределениями классов Y, control group Cи treatment group T.

D(f(u_{i}),f(u_{j})) \rightarrow  \text {max}

Пусть существуют дискретные распределенияQ = (q_{1}, ..., q_{n})иP = (p_{1}, ..., p_{n}), тогда:

\begin{cases}       D \geq 0 \\ D =0, \forall D(Q) = D(P)  \\      \end{cases}

То есть, дивергенция всегда неотрицательная, и дивергенция двух распределений равна нулю, тогда и только тогда, когда распределения абсолютно идентичны. На практике известны и часто используются три меры в качестве расчета дивергенции Dдля распределений :

\begin{cases}      KL(p, q) = \sum_{i}p_{i} \frac{p_{i}}{q_{i}} & \text{ Kullback-Leibler distance} {}\\ E= \sum_{i}(p_{i}-q_{i})^{2} & \text{ Euclidean distance}\\       \chi^{2} = \sum_{i}\frac{(p_{i} - q_{i}}{q_{i}} & \text{ chi-squared divergence}      \end{cases}

Мы не будем останавливаться здесь на преимуществах и недостатках каждой из этих мер, а скажем лишь то, что в дальнейшем будем использовать в качестве меры дивергенции евклидово расстояние между распределениями E(p,q), ввиду его симметричности и существовании в случае, если q_{i}=0. Сразу же переложим математическое определение E(p,q) на код.

def eu_dN(arr):
    
    """"
    --/ euclidean divergence measure
    
    intput : 
        -- | arr : numpy.ndarray
            - format:
            --| arr[:,:-2] - feauters
            --| arr[:, -2] - treatment status (W)
            --| arr[:, -1] - target (Y)
    
    output : 
        --| euclidean distance : float
        --| statistics : dict
        - format:
            --| ed  - euclidean distance
            --| Nc  - size of control group
            --| Nt  - size of treatment group
            --| N   - total size
            --| Pc0 - probability distribution of the attribute Y estimated on the control sample (Y=0)
            --| Pc1 - probability distribution of the attribute Y estimated on the control sample (Y=1)
            --| Pt0 - probability distribution of the attribute Y estimated on the treatment sample (Y=0)
            --| Pt1 - probability distribution of the attribute Y estimated on the treatment sample (Y=1)
    """    
    A = arr[:,-2]  == 0
    Y = arr[:,-1]  == 0   
    cgroup, tgroup = arr[A], arr[~A]
    Nc, Nt         = cgroup.shape[0], tgroup.shape[0]
    
    match [Nc, Nt]:
        case [0, 0]:
            Pc0, Pc1, Pt0, Pt1 = (0, 0, 0, 0)  

        case [0, _]:
            Pc0, Pc1 = (0, 0) 
            Pt0, Pt1 = np.array([np.where(tgroup[:,-1] == 0)[0].size, np.where(tgroup[:,-1] == 1)[0].size]) / Nt

        case [_, 0]:
            Pc0, Pc1 = np.array([np.where(cgroup[:,-1] == 0)[0].size, np.where(cgroup[:,-1] == 1)[0].size]) / Nc
            Pt0, Pt1 = (0,0)

        case _:
            Pc0, Pc1 = np.array([np.where(cgroup[:,-1] == 0)[0].size, np.where(cgroup[:,-1] == 1)[0].size]) / Nc
            Pt0, Pt1 = np.array([np.where(tgroup[:,-1] == 0)[0].size, np.where(tgroup[:,-1] == 1)[0].size]) / Nt
            
    ed = np.square([Pt1 - Pc1])[0] + np.square([Pt0 - Pc0])[0] # euclidean divergence
    
    return ed, {'Nc':Nc, 'Nt':Nt,'N':Nc + Nt, 'Pc0':Pc0, 'Pc1':Pc1, 'Pt0':Pt0, 'Pt1':Pt1}

Но здесь, как и в случаях с энтропией (entropy) или коэффициентом Джини (gini), применяемых в классических деревьях как мера информативности о разбиении, нам интересно не абсолютное значение дивергенции распределений control and treatment groups D(P^{T}(Y):P^{C}(Y)), а прирост дивергенции по результату теста A. Насколько каждый тест улучшает разбиение (расстояние между распределениями), чтобы выбрать лучшее значение и сделать разбивку с его помощью.

D_{gain} = D(P^{T}(Y):P^{C}(Y) | A) -  D(P^{T}(Y):P^{C}(Y))

где D(P^{T}(Y):P^{C}(Y) | A) условная дивергенция двух дочерних нодов, которая определяется следующим образом:

\begin{cases}       D(P^{T}(Y):P^{C}(Y) | A) = \sum \frac{N_{a}}{N}D(P^{T}(Y|a):P^{C}(Y|a)) \\ N = N^{T} + N^{C}  \\ N(a) = N(a)^{T} + N(a)^C      \end{cases}

Осталось сделать последние штрихи, чтобы D_{gain} работал корректно и отображал реальный прирост информации после разбиения теста A:

  1. Ввести штраф за ассиметричное (неравное) разбиение пользователей u \in C и u_ \in T по результатам теста A после разбиения среди дочерних нодов.

  2. Ввести штраф за ассиметричное распределение вероятностей распределений P^{T}(Y) по результатам теста A после разбиения среди дочерних нодов.

  3. Ввести штраф за ассиметричное распределение вероятностей распределений P^{C}(Y) по результатам теста A после разбиения среди дочерних нодов.

Итого, после всех штрафов, прирост информации D_{gain} будет называться нормализованным приростом информации D_{gain}^{norm} и выглядеть следующим образом:

D_{gain}^{norm} = gini(\frac{N^{T}}{N},\frac{N^{C}}{N})D(P^{T}(A):P^{C}(A)) + \frac {N^{T}}{N}gini(P^{T}(A)) + \frac {N^{C}}{N}gini(P^{C}(A)) + \frac{1}{2}

def gini(x):
    "gini"
    arr = np.array(x)
    return 1 - np.sum(arr**2)


def dN_gain(arr, predicat, f):
    
    """
    --/ normalized divergence gain
    
    intput : 
        -- | (arr) arr : numpy.ndarray
            - format:
            --| arr[:,:-2] - feauters
            --| arr[:, -2] - treatment status (W)
            --| arr[:, -1] - target (Y)
            
        -- | (predicat) predicat : numpy.ndarray (boolean mask)
        -- | (f)        divergence function (euclidean/ Kullback–Leibler/ chi-squared)
    output :
        --normalized divergence gain : float
    """
    
    if arr.size == 0:
        return 0
    
    else:
        predicat = predicat.astype(bool)
        l, r   = arr[predicat], arr[~predicat]
        Nl, Nr = l.shape[0], r.shape[0]
        N      = Nl + Nr
        
        if (Nl==0) or (Nr==0):
            return 0
        
        else:
            rootED,  rootDSO   = f(arr)
            lED, lDSO          = f(l)
            rED, rDSO          = f(r)
            
            #weighted divergence gain 
            ed_gain       = (Nl/N)*lED + (Nr/N)*rED - rootED 
            
            #treatment and control shares
            SHARc, SHARt  = np.array([rootDSO['Nc'], rootDSO['Nt']]) / rootDSO['N']
            
            #imbalance penalty
            IMBAr         = gini([SHARc,SHARt])

            if (lDSO['Nt'] == 0) or (rDSO['Nt'] == 0):
                IMBAt = 0
                
            else:
                #treatment distr. imbalance penalty
                IMBAt      = gini([lDSO['Nt'] / rootDSO['Nt'], rDSO['Nt'] / rootDSO['Nt']])

            if (lDSO['Nc'] == 0) or (rDSO['Nc'] == 0):
                IMBAc = 0
                
            else:     
                #control distr. imbalance penalty
                IMBAc      = gini([lDSO['Nc'] / rootDSO['Nc'], rDSO['Nc'] / rootDSO['Nc']])
            
            # normalized divergence gain
            J = (ed_gain * IMBAr) + (SHARt * IMBAt) + (SHARc * IMBAc) + 0.5
        
    return J

Несколько прикладных наблюдений, которые можно сделать, глядя на D_{gain}^{norm}:

  • D_{gain}^{norm} будет минимальным, тогда и только тогда, когда распределение Y в treatment group T и control group C идентичные.

P^{T}(Y|A) = P^{C}(Y|A)

T-learner model

Теперь у нас есть весь инструментарий, чтобы дать описание того как работает T-learner модель. Мы должны написать кастомное рекурсивное дерево M_t (Decision Tree), с разбиениямиA и собственным критерием разбиения (splitting criteria) D_{gain}^{norm} (python scikit-learn не поддерживает из коробки дивергенции как критерий разбиения, поэтому будем писать свой собственный — кастомный). M_t будет иметь небольшую глубину (4–6), чтобы избежать переобучения и минимальное количество элементов в ноде, как одно из условий выхода из рекурсии. Так же надо учесть и реализовать pruning дерева M_t, но это мы опишем уже в следующей статье, которая будет посвящена полной кодовой реализации M_t. В итоге нас интересуют нелистевые ноды (non-leaf nodes), в которые как в корзинки будут попадать пользователи принадлежащие той или иной субпопуляции.

Рис.1 Рекурсивное дерево

Рис. 1 Рекурсивное дерево

Из Рис. 1 мы видим, что тестыA берут на себя формирование активных аудиторий, а критерий разбиения D_{gain}^{norm} выполняет параллельно роль формирования реактивных аудиторий. В итоге, в зависимости от выбранной нами глубины, мы получим конечное количество non-leaf nodes или buskets, которые будут содержать пользователей принадлежащих одной и той же субпопуляции. Далее эти субпопуляции можно будет использовать как «дистиллированные» датасеты для обучения следующей модели, которая отвечает за расчет уже непосредственно самого конечного инкремента \tau, но об этом мы поговорим уже не в этой статье. В следующей работе мы полностью допишем кастомное рекурсивное дерево M_{t}, которое частично начали описывать уже в этой статье.

© Habrahabr.ru