# 02. Массивы NumPy: shape, axis, dtype, indexing и broadcasting > NumPy хранит однородные данные в многомерной форме и выполняет операции над целыми осями. ## Мотивация В ML размерности несут смысл: строки могут быть объектами, столбцы — признаками, третья ось — каналами. Многие ошибки NumPy не завершаются исключением: broadcasting строит допустимый массив неправильного смысла. Поэтому форму и оси нужно читать как часть типа. ## Модель `ndarray` Массив NumPy описывается: - `shape` — длиной каждой оси; - `ndim` — числом осей; - `dtype` — типом каждого элемента; - `size` — общим числом элементов; - `strides` — шагами в байтах при движении по осям. ```python import numpy as np X = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) print(X.shape, X.ndim, X.dtype, X.size, X.strides) ``` \(\mathbf{X}\in\mathbb{R}^{n\times d}\) при \(n=2,d=3\) соответствует форме `(2, 3)`: axis 0 перечисляет объекты, axis 1 — признаки. Массив формы `(d,)` не является ни строкой `(1, d)`, ни столбцом `(d, 1)`. У одномерного массива транспонирование ничего не меняет: ```python x = np.array([1.0, 2.0, 3.0]) print(x.shape, x.T.shape) print(x[:, None].shape, x[None, :].shape) ``` ## `dtype` и численная семантика Однородный `dtype` позволяет хранить данные компактно и выполнять операции в скомпилированных циклах. Но тип ограничивает диапазон и точность. ```python import numpy as np a = np.array([120, 120], dtype=np.int8) print(a + np.array([10, 10], dtype=np.int8)) # переполнение b = np.array([1, 2, 3], dtype=np.float64) print(b / 2) ``` Целые типы переполняются по модулю без обязательной ошибки. `float32` занимает меньше памяти и часто быстрее на GPU, но хранит меньше значащих цифр, чем `float64`. Сравнивать вычисленные float через `==` обычно неверно; нужен допуск. `astype` создаёт преобразованный массив: ```python x = np.array([1.2, 2.9]) print(x.astype(np.int64)) # усечение к нулю: [1 2] ``` ## Создание и изменение формы ```python import numpy as np zeros = np.zeros((2, 3), dtype=np.float64) grid = np.arange(12).reshape(3, 4) points = np.linspace(0.0, 1.0, num=5) print(zeros, grid, points, sep="\n") ``` В `reshape(3, -1)` NumPy выводит одну неизвестную длину. Общее число элементов должно сохраняться. `ravel()` стремится вернуть view, `flatten()` всегда копирует. Объединение не равно добавлению оси: - `np.concatenate` соединяет по существующей оси; - `np.stack` создаёт новую ось; - `np.squeeze` удаляет только оси длины 1; - `np.expand_dims` добавляет ось. ## Индексирование, view и копия Базовый slice обычно возвращает **view** на те же данные: ```python import numpy as np x = np.arange(6) view = x[1:4] view[0] = 99 print(x) # исходный массив изменился ``` Продвинутое индексирование списком индексов или маской обычно создаёт копию: ```python x = np.arange(6) selected = x[[1, 3, 5]] selected[0] = 99 print(x) ``` Не следует строить логику только на слове «обычно»: если владение памятью важно, создавайте явную `copy()` и проверяйте `np.shares_memory`. Булева маска должна соответствовать индексируемой оси: ```python X = np.array([[1, 10], [2, 20], [3, 30]]) mask = X[:, 0] >= 2 print(X[mask]) ``` ## Оси и редукции Редукция уничтожает выбранную ось. Для \(\mathbf{X}\in\mathbb{R}^{n\times d}\): \[ \mu_j=\frac1n\sum_{i=1}^{n}X_{ij} \] усредняет axis 0 и возвращает вектор признаков формы `(d,)`. ```python import numpy as np X = np.array([[1.0, 10.0], [3.0, 14.0], [5.0, 18.0]]) feature_means = X.mean(axis=0) row_means = X.mean(axis=1) print(feature_means.shape, feature_means) print(row_means.shape, row_means) ``` `keepdims=True` сохраняет редуцированную ось длины 1. Это помогает сделать последующий broadcasting намеренным: ```python means = X.mean(axis=0, keepdims=True) # (1, d) centered = X - means # (n, d) ``` ## Broadcasting: точное правило Формы сравниваются справа налево. Две размерности совместимы, если: 1. они равны; или 2. одна из них равна 1; или 3. у одной формы ось отсутствует. Для форм `(4, 3, 2)` и `(3, 1)`: ```text 4 3 2 3 1 ----- 4 3 2 ``` Последняя ось: `2` и `1`; следующая: `3` и `3`; ведущая отсутствует и считается совместимой. Данные логически растягиваются без обязательного физического копирования. ## Разобранный пример: стандартизация признаков Дано \(\mathbf{X}\in\mathbb{R}^{3\times2}\). Для каждого признака: \[ \mu_j=\frac13\sum_{i=1}^3 X_{ij},\qquad \sigma_j=\sqrt{\frac13\sum_{i=1}^3(X_{ij}-\mu_j)^2}, \qquad Z_{ij}=\frac{X_{ij}-\mu_j}{\sigma_j}. \] Для первого столбца `[1,3,5]`: \(\mu_1=3\), \(\sigma_1=\sqrt{(4+0+4)/3}=\sqrt{8/3}\). Для второго `[10,14,18]`: \(\mu_2=14\), \(\sigma_2=\sqrt{32/3}\). ```python import numpy as np X = np.array([[1.0, 10.0], [3.0, 14.0], [5.0, 18.0]]) mean = X.mean(axis=0, keepdims=True) std = X.std(axis=0, keepdims=True) if np.any(std == 0): raise ValueError("Обнаружен константный признак") Z = (X - mean) / std print("shapes:", X.shape, mean.shape, std.shape, Z.shape) print("mean:", Z.mean(axis=0)) print("std:", Z.std(axis=0)) ``` Формы `(3,2) - (1,2)` совместимы: строка средних применяется к каждому объекту. Проверка результата подтверждает среднее около нуля и стандартное отклонение 1. ## Batch-пример: линейный слой `XW + b` Пусть в batch \(n\) объектов, у каждого \(d\) входных признаков, а слой создаёт \(k\) выходов: \[ \mathbf X\in\mathbb R^{n\times d},\quad \mathbf W\in\mathbb R^{d\times k},\quad \mathbf b\in\mathbb R^k. \] Тогда `X @ W` имеет форму `(n, k)`, а `b` формы `(k,)` распространяется по строкам batch. Результат `Y = X @ W + b` также имеет форму `(n, k)`. ```python import numpy as np X = np.array([[1., 2., 3.], [4., 5., 6.]]) # (n=2, d=3) W = np.array([[1., 0.], [0., 1.], [1., 1.]]) # (d=3, k=2) b = np.array([10., -10.]) # (k=2,) Y = X @ W + b # (n=2, k=2) assert X.shape[1] == W.shape[0] assert b.shape == (W.shape[1],) assert Y.shape == (X.shape[0], W.shape[1]) print(Y) # [[14. -5.], [20. 1.]] ``` Форма `b=(1, k)` дала бы тот же результат, но `(k, 1)` несовместима с `(n, k)` в общем случае и не выражает «один bias на каждый выход». Проверяйте контракт форм явно: допустимый broadcasting ещё не доказывает правильность смысла. ## Матричное и поэлементное умножение Для \(\mathbf{X}\in\mathbb{R}^{n\times d}\) и \(\mathbf{w}\in\mathbb{R}^{d}\) выражение `X @ w` имеет форму `(n,)`. `X * w` выполняет поэлементное умножение с broadcasting и имеет форму `(n,d)`. ```python X = np.array([[1.0, 2.0], [3.0, 4.0]]) w = np.array([0.5, -1.0]) print(X @ w) print(X * w) ``` Обе операции законны, но выражают разные математики. Формы нужно выводить до запуска. ## Пропущенные значения `np.nan` — специальное значение float. Обычные редукции распространяют его: `np.mean([1, np.nan])` даёт `nan`; `np.nanmean` игнорирует его. Игнорирование не всегда корректно статистически и должно быть явным решением. Проверки: ```python x = np.array([1.0, np.nan, np.inf]) print(np.isnan(x)) print(np.isfinite(x)) ``` ## Типичные ошибки - **Перепутанная axis.** Подпишите смысл каждой размерности до редукции. - **Ожидание, что `x.T` превратит `(d,)` в столбец.** Используйте `x[:, None]`. - **Случайный broadcasting `(n,1)` с `(n,)`.** Результат станет `(n,n)`. - **Изменение view.** Делайте `copy()`, если нужен независимый массив. - **Целочисленное переполнение.** Выбирайте диапазон dtype осознанно. - **`np.empty` принимается за нули.** Он содержит неинициализированную память. - **Сравнение float через `==`.** Используйте `np.isclose`/`np.allclose`. - **Поэлементное `*` вместо `@`.** Проверяйте математический смысл и форму. ## Краткое резюме - `shape`, `axis` и `dtype` являются частью контракта массива. - `(d,)`, `(d,1)` и `(1,d)` — разные формы. - Редукция убирает ось; `keepdims=True` сохраняет её для явного broadcasting. - Broadcasting сопоставляет формы справа налево и может скрывать смысловую ошибку. - Базовый slice часто разделяет память, продвинутое индексирование обычно копирует. ## Связанные материалы - Предыдущая глава: [`01_python_data_model.md`](01_python_data_model.md). - Следующая глава: [`03_vectorization_testing.md`](03_vectorization_testing.md). - Практика: [`../assignments/01_vectorization.md`](../assignments/01_vectorization.md). - Практика: [`../assignments/02_linear_layer.md`](../assignments/02_linear_layer.md). - Термины: [`glossary.md`](glossary.md).