Разработайте систему машинного обучения для перевода сообщений или комментариев в новостной ленте, такой как Facebook/LinkedIn.

Тип проблемы машинного обучения

Здесь есть 2 проблемы:

  1. Определить язык поста/комментария, сделанного автором
  2. Перевести пост/комментарий на язык зрителя

Будем считать, что для 1-й задачи у нас уже есть модель классификации языков. Это может быть многоклассовый классификатор softmax, где на выходе получается распределение вероятностей по всем возможным языкам.

Наше основное внимание здесь сосредоточено на второй проблеме.

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

Мы можем использовать последовательное обучение с сетями кодировщик-декодер на основе LSTM или Transformer.

Каковы особенности?

Необработанными функциями модели являются слова из исходного и целевого языков.

Например, если исходный язык — английский, а целевой — хинди:

source: “How are you ?”
target: “Aap kaise hai ?”

Затем слова «Как», «есть», «ты» из источника и «Аап», «каисе», «хай» из целевого.

Чтобы обрабатывать орфографические ошибки и отсутствующие символы в производстве, вместо целых слов мы можем использовать символьные n-граммы. Например, с 2-символьными n-граммами у нас будут следующие особенности:

source: ["Ho", "ow", "w ", " a", "ar", "re", ...]
target: ["Aa", "ap", "p ", " k", "ka", "ai", ...]

Для k-символьных n-грамм у нас есть скользящее окно размера k по исходному и целевому предложениям с размером шага 1. Вместо использования одного k мы также можем использовать диапазон k, такой как k=1, чтобы 3. Например. если фактическим словом является «Как», и оно ошибочно набрано как «H0w», при k = 2 мы получим [«H0», «0w»], оба из которых отсутствуют в словаре (OOV). Но при k=от 1 до 2 у нас было бы ["H", "0", "w", "H0", "0w"], у нас есть 2 из 5 функций ("H" и "w"), которые также должно было быть частью правильного слова.

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

  • POS и NER теги
  • isNumeric, isTitle, isAlphanumeric и т. д.

Как получить данные для обучения и тестирования, как получить метки?

Для разработки модели перевода нам потребуются пары предложений для каждой пары языков.

Предполагая, что наша система может работать с 50 различными языками, мы должны иметь 50*50=2500 различных моделей, соответствующих каждой языковой паре. Вот некоторые стратегии получения обучающих данных:

  • Явная маркировка. Создавайте пары предложений вручную с помощью языковых экспертов. Но для создания достаточного количества данных для обучения потребуется много времени.
  • Используйте один язык привязки — например, используя только английский язык, сгенерируйте переводы для оставшихся 49 языков с помощью Google Translate или какой-либо другой существующей службы.
    Мы можем создать файлы CSV с 50 столбцами, каждый из которых соответствует языку, причем первый столбец содержит предложение с якорным языком.

Для каждой модели M(a, b), где a — исходный язык, а b — целевой язык, мы можем извлечь из каждой строки предложения из двух столбцов, соответствующих языкам a и b:

df = pandas.read_csv('data.csv')
df_a_b = df[['language_a', 'language_b']]
where df is the Pandas dataframe corresponding to the CSV file training dataset.

Это предполагает переходное свойство языков, т.е. если A-›переводится в-›B и A-›переводится в-›C, то B-›переводится в-›C, что может не соответствовать действительности в определенных сценариях.

Как выбрать предложения для якорного языка?

  • Выберите предложения разной длины.
    Если мы выберем только длинные предложения, то модель может не усвоить должным образом «значение» коротких фраз, что необходимо для перевода более длинных предложений. Точно так же, если мы выбираем только короткие предложения, система может научиться формировать «неполные» целевые предложения только тогда, когда мы вводим длинные исходные предложения.
  • Из различных доменов, таких как наука, политика, новости, спорт и т. д., чтобы модель не была смещена в сторону слов/фраз из определенных доменов.
  • Использование предложений из исторических сообщений/комментариев, чтобы данные были из похожих доменов. Например, предложения в постах/комментариях в социальной сети более неформальны и могут содержать мемы, сарказмы, аббревиатуры и т. д., что будет сильно отличаться, если вместо этого мы будем использовать только статьи из Википедии.
  • Добавление случайных орфографических ошибок в предложения или случайное удаление слов между предложениями для имитации производственных сценариев.

Какие варианты существуют вместо обучения O(N²) моделей для N языков?

Одним из вариантов является обучение моделей 2N, но в формате бинарного дерева.

Используя якорный язык A в качестве корня, обучите 2 модели с ним в качестве источника и цели в качестве B и C и 2 модели с A в качестве цели и источника в качестве B и C. Аналогичным образом для каждого B и C обучите 2 модели в качестве источника и 2 модели. модели в качестве целей и так далее.

Одним из способов перевода языка X в Y является выполнение поиска в глубину от X до тех пор, пока мы не найдем Y. На каждом этапе мы вызываем модель перевода для перевода P в Q по пути. Количество вызываемых моделей равно O(N).

Другой подход — использование Binary Uplifting + Lowest Common Ancestor.

Вычислите предков каждого узла (1, 2, 4, 8,…), используя стратегию Binary Uplifting. Пространственная сложность — O(N*loglogN). Потому что максимальная глубина от корня до любого конечного узла равна O(logN).

Чтобы преобразовать язык X в Y, сначала мы находим наименьшего общего предка Z для X и Y, затем переводим X в Z за O(logN) вызовов модели, а затем переводим Z в Y за другое O(logN) ) вызовы модели. В худшем случае мы должны пройти диаметр дерева, то есть O(2*logN).

Это снова предполагает, что языки являются транзитивными, т. е.

A->B, B->C implies A->C.

Компромисс здесь заключается в том, что хотя мы обучаем модели O (N) вместо O (N²), но во время вывода нам нужно оценивать модели O (logN) вместо моделей O (1).

Какие шаги предварительной обработки функции необходимы?

  • Удалить выбросы – предложения, состоящие из одного слова, в которых встречаются очень редко (частота равна 1 словам), могут быть удалены. Точно так же предложения с более чем 50% слов, не входящих в словарь, также могут быть удалены из набора обучающих данных.
  • Замените не-UTF8 и специальные символы пробелами.
  • Отметка [начало] и [конец] — для каждого целевого предложения добавьте [начальное] слово в начале и [конечное] слово в конце. Это будет использоваться, чтобы сигнализировать, когда начинать и останавливать создание новых слов на целевом языке.
  • Заменить знаки препинания — знаки препинания на целевом языке заменяются специальными словами. Например, «?» будет заменен на [?] и так далее.
  • В нижнем регистре слов.

Как вычислить представления функций?

Чтобы обучить модели LSTM или Transformer, нам нужно векторизовать функции слова и символа n-граммы:

  1. Отсортируйте все символьные n-граммы (k-символьные n-граммы сверху) и сохраните их в списке.
  2. В данном предложении преобразуйте каждую k-символьную n-грамму в соответствующий индекс в отсортированном списке сверху. Используйте index+1 вместо index, потому что мы хотим начать вычисления с 1.
"How are you" -> ["ho", "ow", "w ", " a", "ar", "re", ...] -> [67, 150, 239, 13, 100, ...]

Поскольку обучение сетей LSTM или Transformer требует, чтобы все предложения в одном пакете имели одинаковую длину, таким образом, предполагая, что максимальная длина любого предложения равна N, все предложения меньше N будут дополнены в конце. с 0 (поскольку мы начинаем наши значения функций с 1 и выше).

[67, 150, 239, 13, 100, 45, 0, 0, 0, 0, 0....0]

После этого каждый вектор предложения длины N затем преобразуется в двумерную матрицу горячего кодирования с размерами N * V, где V — размер словаря.

[[0,0,...0,1,0,0,...0],
 [0,0,0,....0,0,1,..0],
...]

Где хранить представления объектов?

Нам нужно сохранить отсортированный список n-грамм в словаре для всех языков, а также инвертированный индекс (HashMap) от n-граммы к индексу. Оба они могут храниться в Redis. Для сохранения мы также можем хранить их в Cassandra.

Таблица: лексика в Cassandra

(word_id, word, language, index)

В Редисе:

HashMap A: (word, language) -> index
HashMap B: (language, index) -> word

Во время обучения модели мы также будем вычислять вложения для слов, используя слой встраивания. Каждое встраивание, соответствующее функции, будет храниться в Redis HashMap для вывода в реальном времени:

HashMap (key:feature_id) -> [embedding]

Поскольку мы будем обучать модели с использованием сетей Transformer, чтобы сохранить информацию о местоположении, связанную с каждым словом (как в RNN или LSTM), мы будем использовать позиционные вложения отдельно из вложений слов.

pos_embed(i, 2j) = sin(i/10000^(2j/d))
pos_embed(i, 2j+1) = cos(i/10000^(2j/d))

то есть для вложения длины d для i-го признака при каждом нечетном индексе (2j+1) вычислить косинус, а при каждом четном индексе вычислить значение синуса.

Окончательное встраивание — это сумма встраивания слов и pos_embed.

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

Как обучить модель, используя функции?

Подход 1: использование кодировщика-декодера LSTM Seq-To-Seq

Этап обучения:

  • Каждая n-грамма для исходного языка преобразуется в целочисленный индекс, а затем выполняется однократное кодирование перед передачей на уровень внедрения.
  • Слой внедрения перед кодировщиком используется для преобразования одного горячего кодирования n-грамм в плотный вектор.
  • Плотный вектор из слоя внедрения передается кодировщику LSTM. Каждый модуль LSTM вычисляет внутреннее состояние ячейки «c» и скрытое состояние «h», которые передаются следующему модулю.
  • Скрытое состояние «h» из последней единицы кодера используется в качестве входного состояния для сети декодера.
  • Цель декодера смещена влево на одну позицию от входа для декодера. т. е. если ввод [[start], «aap», «kaise», «hai», [end]], то соответствующая цель — [«aap», «kaise», «hai», [end]]. Таким образом, [start] в качестве входных данных используется для прогнозирования «aap», «aap» в качестве входных данных используется для прогнозирования «kaise» и так далее.
  • Каждый блок декодера LSTM выдает выходные данные, кроме состояний «c» и «h». «c» и «h» передаются в следующую ячейку, а выходные данные проходят через плотный слой, а затем слой softmax, который выдает вероятности для каждого слова в позиции t.
  • Затем каждая единица обучается с использованием категориальной функции кросс-энтропийных потерь с фактическим целевым словом в позиции t.

Шаг вывода:

  • Как и при обучении, сеть кодировщика создает состояния «c» и «h», используя исходный язык в качестве входных данных. Эти состояния используются в качестве входных состояний для сети декодера.
  • Во время вывода у нас нет истинного перевода в качестве входных данных, как на этапе обучения.
  • Предсказать слово с наибольшей вероятностью в позиции t. Используйте это слово в качестве входных данных вместе с состояниями «c» и «h» из текущего модуля в качестве входных данных для следующего элемента ячейки.
  • Повторяйте это, пока не встретите [конец] или максимальную длину целевого предложения.

Подход 2: использование Transformer Network с многоуровневым вниманием

Вместо использования LSTM мы используем внимание, чтобы определить, насколько важна n-грамма по сравнению со всеми другими n-граммами.

  • Хотя LSTM справляется с проблемой исчезающего градиента лучше, чем RNN, но все же он не может обрабатывать зависимости очень большого диапазона, когда входные и выходные предложения очень длинные.
  • LSTM является последовательным по своей природе и поэтому не может быть распараллелен. В сетях внимания мы можем распараллелить расчет веса для каждой единицы.

Ниже приведены шаги для обучения модели:

  • Разделите набор данных на обучение, тестирование и проверку (обычно 80–20).
  • Выполните настройку гиперпараметров (количество головок, размер встраивания, количество стеков, отсев и т. д.), используя перекрестную проверку поиска по сетке (K-fold).
  • Лучшие гиперпараметры выбираются на основе показателя BLEU в наборе данных проверки, усредненного по всем K прогонам.
  • Окончательная модель обучается на всех обучающих данных с лучшими гиперпараметрами.

Как оценить модель в офлайне? Метрики?

БЛЕУ Оценка

Как сохранить модель и вес модели, архитектуру и т.д.?

Модели Tensorflow можно сохранять разными способами.

  • Во время обучения, т.е. после каждой эпохи, мы можем сохранять контрольные точки модели с помощью обратных вызовов (так что, даже если мы прекратим обучение, мы сможем начать с того места, где мы остались). Контрольные точки модели состоят из весов и индексных файлов модели. Если сеть сегментирована, то веса, соответствующие каждому сегменту, хранятся в отдельных файлах, а индексный файл похож на хэш-карту от веса (исходный слой, исходный узел, целевой слой, целевой узел) к сегменту.
  • После обучения модель можно сохранить в формате SavedModel, в котором модель хранится в виде файла Protobuf.
  • Все эти контрольные точки и файлы protobuf можно загрузить в S3 после завершения обучения.

‹bucket_name›/‹дата›/‹версия›

Как внедрить коды логического вывода в производство?

  • Вместо того, чтобы явно загружать файл Tensorflow SavedModel и другие индексные файлы для вывода внутри Flask, мы можем использовать TF Serving, чтобы сделать это за нас.
  • Но TF Serving не выполняет предварительную обработку функций. Для этого нам понадобится Flask.
  • Создайте конечные точки Flask для логических выводов.
  • Чтобы сделать Flask многопоточным, используйте Gunicorn поверх Flask.
  • Используйте Github+Jenkins для создания конвейера CI/CD.
  • Всякий раз, когда в Github создается PR для кодов логического вывода, запускайте сборку Jenkins, т. е. запускайте любые модульные и интеграционные тестовые случаи, и, если все тестовые случаи пройдены, затем собирайте образ docker и развертывайте образ докера в кластере Kubernetes.
  • Вместо одного док-контейнера, на котором запущены и TF Serving, и Flask+Gunicorn, создайте 2 отдельных док-контейнера, чтобы они не были связаны.
  • В Kubernetes у нас будет 2 развертывания и 2 службы, соответствующие серверу TF Serving и Flask/Gunicorn.
  • Используйте как минимум 3 реплики для модулей, в которых запущены службы Docker, чтобы обеспечить балансировку нагрузки.

Как контролировать модели в производстве?

Настройте ведение журнала и возможность наблюдения с помощью Datadog или Cloudwatch (при использовании AWS).

Вот некоторые показатели производительности, которые мы можем отслеживать:

  • Количество ошибок 5xx за последние 5 минут.
  • Задержка P99 каждые 5 минут.
  • Количество запросов в секунду.
  • Загрузка ЦП на разных узлах, на которых запущена служба логического вывода.
  • Использование памяти на разных узлах, на которых запущена служба логического вывода.
  • Количество исключений за последние 5 минут.

Метрики модели, которые мы должны отслеживать:

  • Дрейф данных. Для каждого признака предварительно вычислите среднее значение и дисперсию в наборе обучающих данных. В рабочей среде вычисляйте среднее значение и дисперсию значений признаков в потоковом режиме, а через каждые 5 минут вычисляйте Расхождение KL между обучающими и производственными значениями.

Как сделать онлайн-оценку?

Одна метрика, которую мы можем использовать, — это количество взаимодействий с каждым сообщением после того, как какой-либо пользователь нажимает кнопку «Перевести». Взаимодействия здесь означают «Нравится», «Комментарий», «Поделиться», «Отправить запрос на добавление в друзья», «Подписаться на запрос» и т. д.

Создайте таблицу для отслеживания действий пользователей над публикациями, например:

(post_id, user_id, активность, отметка времени)

Затем выберите количество всех действий после activity=’CLICK_TRANSLATE’ на основе временной метки и user_id.

SELECT post_id, COUNT(activity) as num_activities 
FROM
(SELECT * FROM activities a 
INNER JOIN 
(SELECT user_id, MIN(timestamp) FROM activities WHERE activity="CLICK_TRANSLATE" GROUP BY user_id) b 
ON a.user_id=b.user_id AND a.timestamp > b.timestamp AND a.timestamp >= NOW()-'30 days')
GROUP BY post_id

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

Для каждого поста в модели 0 получите счетчики вовлеченности из приведенного выше запроса, аналогично для всех постов из модели 1. Затем с помощью теста KS проверьте, насколько значительно улучшение взаимодействия с моделью. 1 против модели 0. Мы можем использовать данные о взаимодействии за последние 30 дней.

Трубопровод

Рекомендации