NumPy: Шпаргалка¶
~3 минуты чтения
NumPy -- фундамент всего ML-стека в Python: PyTorch tensors, Pandas DataFrames, sklearn внутри используют ndarray. На собеседовании NumPy-вопросы встречаются в 80%+ ML-интервью (vectorization, broadcasting, view vs copy). Знание подводных камней NumPy -- разница между "пишу код" и "понимаю что происходит под капотом".
Быстрый старт¶
import numpy as np
# Создание
arr = np.array([1, 2, 3, 4, 5])
arr = np.array([[1, 2, 3], [4, 5, 6]])
# Атрибуты
arr.shape # (2, 3)
arr.dtype # int64
arr.ndim # 2
arr.size # 6
Создание массивов¶
# Из Python
np.array([1, 2, 3])
np.array([[1, 2], [3, 4]])
# Специальные массивы
np.zeros((3, 4)) # Нули
np.ones((3, 4)) # Единицы
np.full((3, 4), 7) # Заполнить значением
np.empty((3, 4)) # Неинициализированный (быстрее)
np.eye(3) # Единичная матрица
np.diag([1, 2, 3]) # Диагональная матрица
# Последовательности
np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
np.linspace(0, 1, 5) # [0, 0.25, 0.5, 0.75, 1]
np.logspace(0, 3, 4) # [1, 10, 100, 1000]
# Случайные
np.random.rand(3, 4) # Uniform [0, 1)
np.random.randn(3, 4) # Normal(0, 1)
np.random.randint(0, 10, (3, 4)) # Integers
np.random.choice([1, 2, 3], size=5) # Случайный выбор
np.random.permutation(10) # Перемешанные индексы
# ВАЖНО: np.random.seed() -- глобальный state, лучше default_rng (см. ниже)
# Повторения
np.repeat([1, 2, 3], 3) # [1,1,1,2,2,2,3,3,3]
np.tile([1, 2, 3], 3) # [1,2,3,1,2,3,1,2,3]
Индексация и Slicing¶
arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# Базовая индексация
arr[0] # Первая строка: [1, 2, 3]
arr[0, 1] # Элемент: 2
arr[:, 0] # Первый столбец: [1, 4, 7]
arr[0:2] # Первые 2 строки
arr[0:2, 1:3] # Подматрица
# Fancy indexing
arr[[0, 2]] # Строки 0 и 2
arr[:, [0, 2]] # Столбцы 0 и 2
arr[[0, 1], [1, 2]] # Элементы [0,1] и [1,2]
# Boolean indexing
arr[arr > 5] # Элементы > 5: [6, 7, 8, 9]
arr[arr % 2 == 0] # Чётные
# Присваивание
arr[arr > 5] = 0
arr[:, 0] = [10, 20, 30]
Присваивание массива -- это view, не копия
b = a НЕ копирует данные. b и a указывают на одну память -- изменение b[0] = 999 изменит и a[0]. Slicing тоже возвращает view: c = a[0:3] -- та же ловушка. Для независимой копии используйте b = a.copy(). Это источник #1 неожиданных багов в data pipeline, когда одна функция "портит" данные другой.
Изменение формы¶
arr = np.arange(12)
arr.reshape(3, 4) # Новая форма (возвращает view)
arr.reshape(3, -1) # -1 = вычислить автоматически
arr.flatten() # 1D копия
arr.ravel() # 1D view (если возможно)
arr.T # Транспонирование
arr.transpose(1, 0, 2) # Перестановка осей
arr.swapaxes(0, 1) # Поменять 2 оси
np.expand_dims(arr, axis=0) # Добавить ось
np.squeeze(arr) # Убрать оси размера 1
# Stacking
np.vstack([a, b]) # Вертикально
np.hstack([a, b]) # Горизонтально
np.stack([a, b], axis=0) # По новой оси
np.concatenate([a, b], axis=0)
# Splitting
np.split(arr, 3) # На 3 части
np.hsplit(arr, 3) # Горизонтально
np.vsplit(arr, 3) # Вертикально
Математические операции¶
Поэлементные¶
a + b, a - b, a * b, a / b
a ** 2 # Степень
np.sqrt(a) # Корень
np.exp(a) # e^x
np.log(a) # ln(x)
np.log10(a) # log10(x)
np.abs(a) # Модуль
np.sin(a), np.cos(a), np.tan(a)
np.ceil(a), np.floor(a), np.round(a)
np.clip(a, min, max) # Ограничить диапазон
Агрегации¶
arr.sum() # Сумма всех
arr.sum(axis=0) # Сумма по столбцам
arr.sum(axis=1) # Сумма по строкам
arr.sum(axis=0, keepdims=True) # Сохранить размерность
arr.mean() # Среднее
arr.std() # Стандартное отклонение
arr.var() # Дисперсия
arr.min(), arr.max()
arr.argmin(), arr.argmax() # Индексы мин/макс
arr.cumsum() # Кумулятивная сумма
arr.cumprod() # Кумулятивное произведение
np.percentile(arr, 50) # Медиана
np.percentile(arr, [25, 50, 75]) # Квартили
Линейная алгебра¶
# Матричное умножение
np.dot(A, B) # Или A @ B
np.matmul(A, B) # Или A @ B
# Произведения
np.inner(a, b) # Скалярное произведение
np.outer(a, b) # Внешнее произведение
# Линейная алгебра
np.linalg.inv(A) # Обратная матрица
np.linalg.det(A) # Определитель
np.linalg.eig(A) # Собственные значения/векторы
np.linalg.svd(A) # SVD разложение
np.linalg.norm(a) # Норма вектора
np.linalg.solve(A, b) # Решение Ax = b
np.linalg.lstsq(A, b) # Наименьшие квадраты
Логические операции¶
a > b # Поэлементное сравнение
np.greater(a, b) # То же самое
np.equal(a, b)
np.logical_and(a > 0, a < 5)
np.logical_or(a < 0, a > 5)
np.logical_not(a > 0)
np.all(arr > 0) # Все True?
np.any(arr > 0) # Хоть один True?
np.where(arr > 0, arr, 0) # Условная замена
np.argwhere(arr > 0) # Индексы где True
np.isnan(arr) # Проверка на NaN
np.isinf(arr) # Проверка на inf
np.isfinite(arr)
Сортировка¶
np.sort(arr) # Возвращает отсортированную копию
arr.sort() # Сортирует in-place
np.sort(arr, axis=0) # По столбцам
np.sort(arr, axis=1) # По строкам
np.argsort(arr) # Индексы сортировки
np.lexsort((b, a)) # Сортировка по нескольким ключам
np.partition(arr, 3) # Частичная сортировка (k-th element)
np.argpartition(arr, 3)
Broadcasting¶
# Правила:
# 1. Выравниваем размерности справа
# 2. Размерность 1 растягивается
a = np.array([[1, 2, 3]]) # Shape (1, 3)
b = np.array([[1], [2]]) # Shape (2, 1)
a + b # Shape (2, 3)
# Примеры
arr = np.array([[1, 2, 3],
[4, 5, 6]])
arr - arr.mean(axis=0) # Центрирование по столбцам
arr / arr.sum(axis=1, keepdims=True) # Нормализация по строкам
Broadcasting: ошибки тихие, не падают
Если формы несовместимы для broadcasting, NumPy кинет ValueError -- это хороший исход. Опасность в другом: когда формы совместимы, но вы имели в виду другую операцию. Пример: a.shape = (3, 1), b.shape = (1, 4) -- a * b даст матрицу (3, 4). Если b должен был быть (3,) для поэлементного умножения -- ошибки не будет, но результат неправильный. Всегда проверяйте .shape после операций.
np.float64 vs np.float32: молчаливая потеря скорости
NumPy по умолчанию создает float64 массивы: np.array([1.0, 2.0]).dtype = float64. PyTorch и большинство GPU-фреймворков работают с float32. Если передать float64 в PyTorch, он молча конвертирует, удваивая memory. Хуже: matmul двух float64 матриц в 2x медленнее float32 на одном CPU. Привычка: np.array([...], dtype=np.float32) или arr.astype(np.float32) перед передачей в ML-пайплайн.
Работа с NaN¶
np.nan # Not a Number
np.isnan(arr) # Проверка
np.nansum(arr) # Сумма игнорируя NaN
np.nanmean(arr) # Среднее игнорируя NaN
np.nanstd(arr)
np.nanmax(arr), np.nanmin(arr)
# Замена NaN
arr[np.isnan(arr)] = 0
np.nan_to_num(arr, nan=0) # Заменить NaN на 0
Полезные функции¶
# Уникальные значения
np.unique(arr)
np.unique(arr, return_counts=True)
# Сравнение
np.allclose(a, b, rtol=1e-5) # Примерное равенство
# Копирование
b = a.copy() # Глубокая копия
b = a # Это view!
# Типы данных
arr.astype(np.float32)
arr.astype('int32')
# Сохранение/загрузка
np.save('array.npy', arr)
arr = np.load('array.npy')
np.savez('arrays.npz', a=arr1, b=arr2)
data = np.load('arrays.npz')
arr1 = data['a']
Случайные числа (новый API)¶
rng = np.random.default_rng(seed=42)
rng.random((3, 4)) # Uniform [0, 1)
rng.standard_normal((3, 4)) # Normal(0, 1)
rng.integers(0, 10, (3, 4)) # Integers
rng.choice([1, 2, 3], size=5)
rng.shuffle(arr) # In-place shuffle
rng.permutation(arr) # Возвращает перемешанную копию
Полезные паттерны¶
Нормализация¶
# Min-Max [0, 1]
arr_norm = (arr - arr.min()) / (arr.max() - arr.min())
# Z-score
arr_std = (arr - arr.mean()) / arr.std()
# L2 нормализация
arr_l2 = arr / np.linalg.norm(arr)
One-hot encoding¶
Cosine similarity¶
Batch matrix multiply¶
# A: (batch, n, m), B: (batch, m, k)
C = np.einsum('bnm,bmk->bnk', A, B)
# или
C = A @ B # Broadcasting работает
Вопросы на интервью¶
Q: В чём разница между view и copy в NumPy?
"reshape всегда создает копию" -- неверно, reshape возвращает view когда возможно
"Slicing (
a[0:3]), reshape(), .T возвращают view -- разделяют память с оригиналом. Изменение view меняет оригинал. copy(), flatten() создают независимые копии. Проверить: np.shares_memory(a, b). Это критично в pipeline -- функция может случайно модифицировать входные данные."
Q: Как работает broadcasting?
"NumPy просто копирует массив до нужного размера" -- копирования не происходит
"Broadcasting -- виртуальное растягивание. Правила: выравнивание shapes справа, размерность 1 растягивается, несовместимые размерности (не 1 и не равны) дают ValueError. Пример:
(3,1) + (1,4) = (3,4). NumPy не копирует данные -- использует strides с нулевым шагом. Это O(1) по памяти, но результат -- новый массив (3,4)."
Q: Почему vectorized операции быстрее циклов?
"Потому что NumPy написан на C" -- часть ответа, но не главная
"Три причины: (1) C-реализация без Python interpreter overhead, (2) SIMD-инструкции (AVX2/AVX512) обрабатывают 8-16 float за такт, (3) cache locality -- contiguous memory access.
np.dot(a, b) вызывает BLAS (MKL/OpenBLAS) с 100x ускорением vs Python loop. На массиве 1M элементов: loop = ~500ms, vectorized = ~1ms."
See Also¶
- Pandas Cheatsheet — DataFrame, merge, groupby
- PyTorch Cheatsheet — tensors, GPU, autograd
- Deep Learning Interview Q&A — вопросы про матричные операции
- Math: Linear Algebra — SVD, eigenvalues, матричные разложения