Совместная фильтрация для рекомендации газетных статей

В этой статье показано, как реализовать модель WALS (матричная факторизация) для совместной фильтрации. Для совместной фильтрации нам не нужно ничего знать ни о пользователях, ни о контенте. По сути, все, что нам нужно знать, - это userId, itemId и рейтинг, который конкретный пользователь дал конкретному элементу.

Использование времени, проведенного на странице, в качестве рейтинга

В этом случае мы работаем с газетными статьями. Компания не просит пользователей оценивать статьи. Однако мы можем использовать время, проведенное на странице, как прокси для оценки.

Вот запрос, чтобы получить продолжительность, которую каждый пользователь тратит на каждую газетную статью (обычно мы также добавляем к этому временному фильтру («последние 7 дней»), но сам наш набор данных ограничен несколькими днями):

WITH CTE_visitor_page_content AS (
    SELECT
        # Schema: https://support.google.com/analytics/answer/3437719?hl=en
        # For a completely unique visit-session ID, we combine combination of fullVisitorId and visitNumber:
        CONCAT(fullVisitorID,'-',CAST(visitNumber AS STRING)) AS visitorId,
        (SELECT MAX(IF(index=10, value, NULL)) FROM UNNEST(hits.customDimensions)) AS latestContentId,  
        (LEAD(hits.time, 1) OVER (PARTITION BY fullVisitorId ORDER BY hits.time ASC) - hits.time) AS session_duration 
    FROM
        `cloud-training-demos.GA360_test.ga_sessions_sample`,   
        UNNEST(hits) AS hits
    WHERE 
        # only include hits on pages
        hits.type = "PAGE"
GROUP BY   
        fullVisitorId,
        visitNumber,
        latestContentId,
        hits.time )
      
-- Aggregate web stats
SELECT   
    visitorId,
    latestContentId as contentId,
    SUM(session_duration) AS session_duration
FROM
    CTE_visitor_page_content
WHERE
    latestContentId IS NOT NULL 
GROUP BY
    visitorId, 
    latestContentId
HAVING 
    session_duration > 0
LIMIT 10

Результат выглядит так:

Масштабирование поля рейтинга до [0, 1]

Мы ожидаем, что посетителям, которые потратили больше времени на просмотр статьи, она понравится больше. Однако продолжительность сеанса может быть немного смешной. Обратите внимание на 55440 секунд (15 часов), которые показывает строка №3. Мы можем построить гистограмму продолжительности сеанса в наборе данных:

Итак, давайте отмасштабируем и обрежем его значения по средней продолжительности сеанса (на среднюю продолжительность сильно повлияют выбросы):

...
normalized_session_duration AS (
    SELECT APPROX_QUANTILES(session_duration,100)[OFFSET(50)] AS median_duration
    FROM aggregate_web_stats
)
SELECT
   * EXCEPT(session_duration, median_duration),
   CLIP(0.3 * session_duration / median_duration, 0, 1.0) AS normalized_session_duration
FROM
   aggregate_web_stats, normalized_session_duration

где CLIP определяется следующим образом:

CREATE TEMPORARY FUNCTION CLIP_LESS(x FLOAT64, a FLOAT64) AS (
  IF (x < a, a, x)
);
CREATE TEMPORARY FUNCTION CLIP_GT(x FLOAT64, b FLOAT64) AS (
  IF (x > b, b, x)
);
CREATE TEMPORARY FUNCTION CLIP(x FLOAT64, a FLOAT64, b FLOAT64) AS (
  CLIP_GT(CLIP_LESS(x, a), b)
);

Теперь продолжительность сеанса масштабируется и находится в правильном диапазоне:

Собираем все вместе и материализуем в таблицу:

CREATE TEMPORARY FUNCTION CLIP_LESS(x FLOAT64, a FLOAT64) AS (
  IF (x < a, a, x)
);
CREATE TEMPORARY FUNCTION CLIP_GT(x FLOAT64, b FLOAT64) AS (
  IF (x > b, b, x)
);
CREATE TEMPORARY FUNCTION CLIP(x FLOAT64, a FLOAT64, b FLOAT64) AS (
  CLIP_GT(CLIP_LESS(x, a), b)
);
    
CREATE OR REPLACE TABLE advdata.ga360_recommendations_data
AS
WITH CTE_visitor_page_content AS (
    SELECT
        # Schema: https://support.google.com/analytics/answer/3437719?hl=en
        # For a completely unique visit-session ID, we combine combination of fullVisitorId and visitNumber:
        CONCAT(fullVisitorID,'-',CAST(visitNumber AS STRING)) AS visitorId,
        (SELECT MAX(IF(index=10, value, NULL)) FROM UNNEST(hits.customDimensions)) AS latestContentId,  
        (LEAD(hits.time, 1) OVER (PARTITION BY fullVisitorId ORDER BY hits.time ASC) - hits.time) AS session_duration 
    FROM
        `cloud-training-demos.GA360_test.ga_sessions_sample`,   
        UNNEST(hits) AS hits
    WHERE 
        # only include hits on pages
        hits.type = "PAGE"
GROUP BY   
        fullVisitorId,
        visitNumber,
        latestContentId,
        hits.time ),
aggregate_web_stats AS (      
-- Aggregate web stats
SELECT   
    visitorId,
    latestContentId as contentId,
    SUM(session_duration) AS session_duration
FROM
    CTE_visitor_page_content
WHERE
    latestContentId IS NOT NULL 
GROUP BY
    visitorId, 
    latestContentId
HAVING 
    session_duration > 0
),
normalized_session_duration AS (
    SELECT APPROX_QUANTILES(session_duration,100)[OFFSET(50)] AS median_duration
    FROM aggregate_web_stats
)
SELECT
   * EXCEPT(session_duration, median_duration),
   CLIP(0.3 * session_duration / median_duration, 0, 1.0) AS normalized_session_duration
FROM
   aggregate_web_stats, normalized_session_duration

Модель рекомендаций по обучению

Данные таблицы теперь выглядят так:

Мы можем обучить модель следующим образом:

CREATE OR REPLACE MODEL advdata.ga360_recommendations_model
OPTIONS(model_type='matrix_factorization', 
        user_col='visitorId', item_col='contentId',
        rating_col='normalized_session_duration',
        l2_reg=10)
AS
SELECT * from advdata.ga360_recommendations_data

Примечание. Если вы используете тарифный план по требованию, вы получите сообщение об ошибке Модели факторизации матрицы обучения недоступны для использования по требованию. Totrain, настройте резервирование (гибкое или обычное) в соответствии с инструкциями в общедоступных документах BigQuery . Это связано с тем, что факторизация матрицы имеет тенденцию становиться дорогостоящей, если цена определяется на основе данных. Слоты Flex устанавливаются на основе вычислений и намного дешевле. Итак, настройте слоты flex - вы можете использовать их всего 60 секунд.

Вы будете играть со значением l2_reg, которое дает разумные ошибки для вашего набора данных. Откуда вы знаете, что это разумно? Проверьте вкладку оценки:

Вы хотите, чтобы средняя и медианная абсолютные ошибки были похожи и были намного меньше 0,5 (случайный шанс будет 0,5).

Делать прогнозы

Вот как попасть в топ-3 рекомендаций для каждого пользователя:

SELECT 
  visitorId, 
  ARRAY_AGG(STRUCT(contentId, predicted_normalized_session_duration)
            ORDER BY predicted_normalized_session_duration DESC
            LIMIT 3)
FROM ML.RECOMMEND(MODEL advdata.ga360_recommendations_model)
WHERE predicted_normalized_session_duration < 1
GROUP BY visitorId

Это дает:

Наслаждаться!

Огромное спасибо Луке Семпре и Эвану Джонсу за помощь в получении данных Google Analytics.