NumPy для самых маленьких

—

от автора

Математика везде в нашей жизни, но в программировании, а особенно ML ее два раза больше. Обычно Питон берут в пример самого «научного» языка программирования из-за математических фреймворков. Как не Питон может помочь оперировать математическими абстракциями, некоторые из сферы ресерча пользуются исключительно питоном для всяких научных изысканий — сегодня мы поговорим про библиотеку NumPy и работу с массивами. 

Самая новичковая «библиотека» с примочками в виде SciPy и Matplotlib предназначена для работы с многомерными массивами. NumPy – основа для многих других библиотек для машинного обучения, таких как SciPy, Pandas, Scikit-learn и TensorFlow. 

Pandas, например, строится поверх NumPy и позволяет работать со структурами данных высокого уровня по типу DataFrame и Series. При помощи NumPy можно проводить преобразование категориальных данных в числовой формат, например, с использованием кодирования one-hot.

NumPy реализована на C, на уровне низких абстракций, поэтому вся работа с библиотекой не протекает в формате «ждем два часа компиляции кода» — библиотека написана на низкоуровневом языке для максимальной скорости и эффективности. 

Массивы = работа с классическими матрицами и векторами, иначе многомерными массивами/ndarray. В распоряжении самые простые функции для элементов внутри sin/cos/or/and, линейные операции для самих матриц от нахождения определителя до их перемножения, поддержка векторизации — все это и есть наш кратчайший путь к математическим абстракциям, ML. 

Предварительно поставить утилиту можно через pip install numpy, импортировать через import numpy as np

Пробежимся по самым простым «операциям» в библиотеке. Главный объект — массивы. NumPy предоставляет несколько способов создания массивов. 

Наиболее распространенные из них про создание:

Cоздание массивов из списков. 

Создание массивов заданного размера или формы с начальными значениями 

Массивы нулей или массивы с рандомными элементами. 

Генерируем стандартные списки массивы 

Из списка:

arr = np.array([1, 2, 3, 4, 5], float)

На входе нам предоставлено два аргумента: список, конвертируемый в массив и тип данных. 

Из заданного размера и начальных значений

zeros_arr = np.zeros((3, 3))  # Массив нулей размером 3x3 ones_arr = np.ones((2, 2)) # Массив единиц размером 2x2

Да, внутри Numpy можно быстро задать массив любой размерности с одинаковыми элементами. 

Массив с рандомизированными значениями.

rand_arr = np.random.rand(3, 3)  # Массив 3x3 со случайными значениями

У массивов есть несколько особенностей:

— Размер массива фиксируется и поменяться после создания не может. С одной стороны, мы теряем в гибкости и некотором функционале – с другой, так библиотека выполняет операции с массивами быстрее. 

— Все элементы должны иметь одинаковый тип данных. 

Но задавать можно не только целочисленные значения – можно выбирать тип данных. 

Для создания многомерных массивов, нам следует просто прописать разные оси через запятую. 

matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])  #двумерный массив 3x3.   tensor = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]], [[9, 10], [11, 12]]])  #наш маленький трехмерный тензор 3х2х2. 

Как прооперировать наши миниматрицы?

Индексация и нарезка — это способы доступа к элементам и подмассивам в массивах NumPy. 

Напоминаем, что индексация начинается с 0. 

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

Нарезка позволяет выбирать подмассивы по заданным диапазонам индексов с использованием синтаксиса [start:stop:step], 

где start – начальный индекс (включается), 

stop – конечный индекс (не включается), 

step – шаг. 

Выглядит это примерно так:

arr1d = np.array([1, 2, 3, 4, 5])  # Нарезка для выбора подмассива print(arr1d[1:4])    # Получаем [2 3 4] (элементы с индексами от 1 до 3) print(arr1d[:3])     # Получаем [1 2 3] (элементы до индекса 3) print(arr1d[::2])    # Получаем [1 3 5] (каждый второй элемент)  # Тоже самое можно проделать с двумерными матрицами arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])  # Нарезка для выбора подматрицы print(arr2d[0:2, 1:])    # Вывод: [[2 3] [5 6]] (подматрица с 1 по последний столбец в строках с 0 по 1) print(arr2d[:2, :2])     # Вывод: [[1 2] [4 5]] (подматрица в строках с 0 по 1 и столбцах с 0 по 1)

Как управлять элементами матрицы (творить с ними все, что захочется)

В этом материале мы не будем расписывать, например, применение логического «Или» и «И», так как это слишком просто — совсем новичку стоит открыть документацию. Но есть парочка интересных функций, с которыми можно поработать в перспективе и сократить время на написание «костылей». 

Например, в Numpy есть статистические функции

np.mean() — среднее значение элементов массива.

np.median() — медиана элементов массива.

np.std(): стандартное отклонение элементов массива.

np.var(): дисперсия элементов массива.

np.percentile() — квантили массива.

Или бродкастинг, про который некоторые не знают. 

Например, функция np.broadcast() позволяет выполнять поэлементные операции между массивами различных форм и размерностей. Этот объект не массив, но информирует о том, как выполнять операции с массивами разной формы без явного копирования данных или изменения их формы.

arr1 = np.array([1, 2, 3])         # Форма (3,) arr2 = np.array([[4], [5], [6]])   # Форма (3, 1)  broadcasted = np.broadcast(arr1, arr2)  print(broadcasted.shape)

Мы позаимствовали с Pinterest даже небольшую схемку с отображениями “представлений массивов”.

А еще в NumPy можно работать с многочленами. Если уж любить линейную математику, то до конца. Создать многочлен можно через функцию np.polyid(). И некоторые «специальные» виды уравнений:

np.polynomial.legendre.Legendre() – многочлены Лежандра.

np.polynomial.chebyshev.Chebyshev() – многочлены Чебышева.

np.polynomial.laguerre.Laguerre() – многочлены Лагерра.

Например, полиномы Чебышева используются в исчислимых методах аппроксимации или интерполяции. 

# Создаем полином Чебышева первого рода chebyshev_poly = np.polynomial.chebyshev.Chebyshev([1, 0, -1])  # Многочлен: x^2 - 1  # Вычисление значения многочлена в заданной точке x = 0.5 value = chebyshev_poly(x)  # Интегрирование многочлена integral = chebyshev_poly.integrate()

Работаем с матрицами/массивами по взрослому

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

Умножение матриц (np.dot()), транспонирование матриц (np.transpose()), нахождение обратной матрицы (np.linalg.inv()), решение систем линейных уравнений (np.linalg.solve()) и нахождение собственных значений и собственных векторов (np.linalg.eig()). 

На примере решения системы линейных уравнений:

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

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

Функция np.linalg.solve() решает систему линейных уравнений, которая моделирует это уравнение. Мы представляем эту систему в виде матрицы, где строки представляют собой примеры данных, а столбцы — признаки. Мы находим оптимальные значения весов и смещения путем решения этой системы уравнений. Эти оптимальные параметры позволяют нам построить и оценить линейную модель для наших данных в задачах машинного обучения.

В коде выглядит это как-то так: 

# Создаем матрицы признаков X и вектора целевых переменных y (пример) X = np.array([[1, 2], [3, 4], [5, 6]]) y = np.array([3, 7, 11])  # Решаем линейное уравнение w, b = np.linalg.solve(X.T @ X, X.T @ y)  print(" Тут у нас оптимальные значения весов:") print(w) print(" А тут оптимальное значение смещения:") print(b)

Неочевидные функции:

Да, действительно в NumPy достаточно функций и перечислить все — растянуть материал на пару десятков килознаков. Но есть несколько неочевидных решений, которые упростят работу в моменте. 

np.where() позволяет выполнять условное индексирование. Она возвращает индексы элементов, удовлетворяющих заданному условию. Можно также использовать ее для замены значений в массиве по определенному условию.

arr = np.array([1, 2, 3, 4, 5]) indices = np.where(arr > 2) print(indices)  # Вывод: (array([2, 3, 4]),)

np.unique() –  функция возвращает уникальные значения в массиве в отсортированном порядке. Это может быть полезно для удаления дубликатов или анализа уникальных значений.

arr = np.array([1, 2, 3, 1, 2, 4, 5]) unique_values = np.unique(arr) print(unique_values)  # Вывод: [1 2 3 4 5]

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

arr = np.array([1, 2, 3, 4, 5]) clipped_arr = np.clip(arr, 2, 4) print(clipped_arr)  # Вывод: [2 2 3 4 4]

np.ravel() «выпрямляет» массив, превращая многомерный массив в одномерный. Это может быть удобно для быстрого преобразования массива в одномерный вид.

arr = np.array([[1, 2, 3], [4, 5, 6]]) raveled_arr = np.ravel(arr) print(raveled_arr)  # Вывод: [1 2 3 4 5 6]

Простой пример или проба NumPy на линейной регрессии без сторонних библиотек и фреймворков

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

X_train = np.array([[1], [2], [3], [4], [5]])  # Входные признаки (одномерный массив) y_train = np.array([2, 4, 5, 4, 5])            # Целевая переменная  # Добавляем столбец с единицами для учета свободного члена в модели X_train_with_bias = np.c_[np.ones((X_train.shape[0], 1)), X_train]  # Обучение модели линейной регрессии theta = np.linalg.inv (X_train_with_bias.T.dot(X_train_with_bias)).dot(X_train_with_bias.T).dot(y_train)  # Вывод коэффициентов модели print("Коэффициенты модели:", theta)  # Предсказание на новых данных X_test = np.array([[6], [7]])                 # Новые входные признаки X_test_with_bias = np.c_[np.ones((X_test.shape[0], 1)), X_test] y_pred = X_test_with_bias.dot(theta)  # Вывод предсказанных значений print("Предсказанные значения:", y_pred)

Функция np.linalg.inv() используется для вычисления обратной матрицы.

Затем мы создаем новые входные признаки для предсказания и выполняем предсказание значений целевой переменной с использованием обученных параметров модели. 

И не нужно нам писать: используйте обычные списки. Мы просто посмеемся. Хотя пример карикатурный, но весь смысл NumPy – базовая библиотека для линейных операций, коих в ML много и дальше больше. 

В этой статье мы быстренько пробежались по функционалу библиотеки и даже показали на примере, как построить простую линейную регрессию. Самое большое наставление, которое можно дать любому начинающему ML/Data-специалисту — математика наше все. Благодаря рукотворно написанным ресерчам мы и получили прекрасные GPT, иначе трансформаторы и благодаря линейной алгебре мы можем прописывать «градиентный» бустинг. 

Писать на низком уровне абстракций, мы не про ассемблер и С, попробовать обозначать математику через такие библиотеки как NumPy — путь к лучшему пониманию машинного обучения в целом. 


ссылка на оригинал статьи https://habr.com/ru/articles/809109/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *