Более пяти лет назад я опубликовал свою самую успешную статью на Medium. Она появилась из-за необходимости отфильтровать данные особенно шумного датчика из потока телематических данных. Если говорить конкретно, то это был датчик крутящего момента, подключенный к карданному валу грузовика, и от шума нужно было избавиться. Решение было найдено с помощью внедренного в Python алгоритма LOESS (locally estimated scatterplot smoothing — локально взвешенное сглаживание диаграммы рассеяния). Это решение и было описано в статье.

Тогда я с головой был погружен в Python, а проект требовал использования Spark, поэтому реализовать алгоритм на Python было несложно. Однако теперь чаще использую Rust и решил попробовать перевести на него старый код. В этой статье описан процесс переноса и мой выбор решений при переписывании кода. Чтобы больше узнать об алгоритме, советую обратиться к исходной статье и справочным материалам. А здесь сосредоточимся на тонкостях написания матричного кода на Rust, с максимальной точностью заменяющего раннюю реализацию NumPy.

Численные вычисления на Rust

Не собираясь изобретать велосипед, я поискал рекомендованные Rust-крейты для замены NumPy в исходном Python-коде. Обнаружение nalgebra не заняло много времени.

nalgebra — это библиотека линейной алгебры общего назначения с низкой размерностью и оптимизированным набором инструментов для работы с компьютерной графикой и физикой.

Хотя в данном случае занятия физикой или компьютерной графикой нас не интересуют, требование низкой размерности подходит как нельзя лучше.

Различия

При переводе Python-кода на Rust я столкнулся с некоторыми трудностями, на преодоление которых ушло немало времени. При использовании NumPy в Python доступны все возможности как языка, так и библиотеки для повышения выразительности и читабельности кода. Rust более многословен, чем Python, и на момент написания этой статьи (версия 0.33.0) крейту nalgebra все еще не хватало возможностей, позволяющих повысить выразительность кода. Лаконичность nalgebra — это проблема.

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

Я использовал эту возможность во всем Python-коде:

# Python
xx = self.n_xx[min_range]

Здесь min_range — переменная в целочисленном массиве, содержащем подмножество индексов для извлечения из массива self.n_xx.

Как ни старался, я так не смог найти в Rust-крейте решение, имитирующее индексацию NumPy, поэтому пришлось внедрить его. После нескольких попыток и бенчмарков пришел к финальной версии. Это решение было простым и эффективным.

// Rust
fn select_indices(values: &DVector<f64>,
indices: &DVector<usize>) -> DVector<f64> {
indices.map(|i| values[i])
}

Выражение map довольно простое, но использование имени функции более выразительно, поэтому я заменил приведенный выше Python-код на соответствующий Rust-код:

// Rust
let xx = select_indices(&self.xx, min_range);

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

// Rust
range = DVector::<usize>::from_iterator(window, 0..window);

Можно избежать большей части этой записи, если зафиксировать размеры вектора и массива во время компиляции. Но здесь мне не повезло, поскольку размеры неизвестны. Соответствующий код на Python более лаконичен:

# Python
np.arange(0, window)

Подобная лаконичность распространяется и на другие области, например при построчном заполнении матрицы. В Python можно сделать примерно так:

# Python
for i in range(1, degree + 1):
xm[:, i] = np.power(self.n_xx[min_range], i)

На данный момент я не нашел лучшего способа сделать то же самое с nalgebra, чем этот:

// Rust
for i in 1..=degree {
for j in 0..window {
xm[(j, i)] = self.xx[min_range[j]].powi(i as i32);
}
}

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

Документация по nalgebra показалась мне относительно скудной. Этого можно было ожидать от относительно молодого Rust-крейта, подающего большие надежды на будущее.

Позитивный аспект

Самое интересное ожидало меня в конце — номинальная производительность. Попробуйте запустить обе версии одного и того же кода (ссылки на репозитории GitHub приведены ниже) и сравнить их производительность. На моем MacBook Pro 2019 года с 6-ядерным процессором Intel Core i7 2,6 ГГц релизная версия Rust-кода выполняется менее чем за 200 микросекунд, а Python-код — менее чем за 5 миллисекунд.

Заключение

Этот проект стал еще одним увлекательным и познавательным переносом старого кода с Python на Rust. В то время как преобразование известных управляющих структур Python на рельсы Rust с каждым днем становится все более доступным, переход с NumPy на nalgebra оказался более сложной задачей. Пакет Rust демонстрирует большие перспективы, но нуждается в обширной документации и онлайн-поддержке. Я бы горячо приветствовал более подробное руководство пользователя.

Rust более «протокольный» чем Python, но при правильном использовании работает гораздо лучше. Буду продолжать работать с Python для повседневной работы при создании прототипов и в режиме открытий, но при переходе к производству обращусь к Rust для повышения производительности и безопасности памяти. Думаю, что беспроигрышным вариантом будет смешение и сочетание обоих языков с помощью таких крейтов, как PyO3.

Ссылки:

Ассистент, созданный на основе ИИ JetBrains, помог мне написать часть кода и освоить Rust. Он стал неотъемлемой частью моей повседневной работы как с Rust, так и с Python. К сожалению, поддержка nalgebra все еще недостаточна.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи João Paulo Figueira: LOESS in Rust

Предыдущая статьяТехнологический стек для создания веб-приложений