NumPy: массивы, векторизация и broadcasting

⏱ 35 мин · 🧩 заданий: 5

Зачем NumPy

Нейросеть — это огромное количество умножений и сложений чисел. Если делать их циклами Python, обучение будет идти часами там, где хватит секунд. NumPy хранит числа в плотном массиве в памяти и выполняет операции над всем массивом сразу, в скомпилированном коде на C. Почти всё в ML на Python — PyTorch, scikit-learn, pandas — построено на тех же идеях.

import numpy as np
import time

xs = list(range(1_000_000))
t = time.perf_counter()
squares = [x * x for x in xs]
t_py = time.perf_counter() - t

a = np.arange(1_000_000)
t = time.perf_counter()
sq = a * a
t_np = time.perf_counter() - t

print(f"цикл Python: {t_py * 1000:.1f} мс")
print(f"NumPy:       {t_np * 1000:.1f} мс   (в {t_py / max(t_np, 1e-9):.0f} раз быстрее)")

Массив: форма, тип, оси

Главный объект — np.ndarray, многомерный массив чисел одного типа.

import numpy as np

v = np.array([1, 2, 3])                 # вектор
M = np.array([[1, 2, 3], [4, 5, 6]])    # матрица 2×3

print(v.shape, v.ndim)    # (3,) 1
print(M.shape, M.ndim)    # (2, 3) 2
print(M.size, M.dtype)    # 6 элементов; тип — целые числа

batch = np.zeros((32, 28, 28))     # 32 картинки 28×28 — типичный «тензор» в ML
print(batch.shape, batch.dtype)
  • shape — кортеж размеров по каждой оси. Это самое важное свойство: большинство ошибок в ML-коде — несовпадение форм. Печатайте x.shape при любом сомнении.
  • ось 0 — строки (или объекты в батче), ось 1 — столбцы (признаки), и так далее.
  • dtype — тип элементов. В нейросетях почти всегда float32 или float64.

Тензор

В ML любой многомерный массив называют тензором: число — тензор ранга 0, вектор — ранга 1, матрица — ранга 2, батч цветных картинок (N, 3, H, W) — ранга 4.

Как создавать массивы

import numpy as np

print(np.zeros(3))                # [0. 0. 0.]
print(np.ones((2, 2)))
print(np.arange(0, 10, 3))        # как range: [0 3 6 9]
print(np.linspace(0, 1, 5))       # 5 равномерных точек от 0 до 1 включительно
print(np.eye(3))                  # единичная матрица

rng = np.random.default_rng(42)   # генератор случайных чисел с зерном (seed)
print(rng.normal(0, 1, size=3))   # нормальное распределение
print(rng.integers(0, 10, size=5))

Зерно (seed) делает «случайные» числа воспроизводимыми: при каждом запуске они одинаковые. В ML это важно, чтобы эксперименты можно было повторить.

Индексация, срезы и маски

import numpy as np

M = np.arange(12).reshape(3, 4)
print(M)
print(M[1, 2])       # строка 1, столбец 2 → 6
print(M[0])          # первая строка
print(M[:, 1])       # второй столбец
print(M[1:, ::2])    # строки с 1-й, столбцы через один

mask = M % 2 == 0    # булева маска той же формы
print(mask)
print(M[mask])       # только чётные элементы
M[M > 8] = 0         # присваивание по маске
print(M)

Индексация массивом индексов («fancy indexing») берёт элементы по списку номеров. Это пригодится, например, чтобы выбрать вероятности правильных классов:

import numpy as np

probs = np.array([[0.7, 0.2, 0.1],     # 3 объекта, 3 класса
                  [0.1, 0.8, 0.1],
                  [0.3, 0.3, 0.4]])
targets = np.array([0, 1, 1])          # правильные классы
print(probs[np.arange(3), targets])    # [0.7 0.8 0.3] — вероятность правильного ответа у каждого

Срез — это вид, а не копия

row = M[0] не копирует данные: изменение row изменит M. Если нужна независимая копия — M[0].copy().

Векторизация: операции над целыми массивами

Арифметика с массивами работает поэлементно, без циклов:

import numpy as np

a = np.array([1.0, 2, 3])
b = np.array([10.0, 20, 30])
print(a + b, a * b, b / a)
print(a ** 2, np.sqrt(a), np.exp(a))
print(a @ b)          # скалярное произведение: 1·10 + 2·20 + 3·30 = 140
print(a > 1.5)        # сравнение тоже поэлементно

Пишите код «на уровне массивов», а не элементов. Например, средняя квадратичная ошибка из прошлого урока в одну строку:

import numpy as np

areas  = np.array([32, 38, 45, 50, 56, 63, 70, 78, 85, 95])
prices = np.array([6.8, 7.9, 9.1, 10.4, 11.0, 12.9, 13.8, 15.6, 16.5, 19.2])

w, b = 0.2, 0.5
mse = ((w * areas + b - prices) ** 2).mean()
print(f"MSE = {mse:.4f}")

Агрегации и оси

sum, mean, max, min, std, argmax работают по всему массиву или вдоль выбранной оси. axis=k — это ось, которая исчезнет.

import numpy as np

scores = np.array([[5, 3, 4],      # 2 студента × 3 предмета
                   [2, 5, 5]])
print(scores.sum())                # 24 — всё вместе
print(scores.sum(axis=0))          # [7 8 9] — по каждому предмету (строки схлопнулись)
print(scores.mean(axis=1))         # [4. 4.] — средний балл каждого студента
print(scores.argmax(axis=1))       # [0 1] — номер лучшего предмета у каждого
print(scores.max(axis=1, keepdims=True).shape)   # (2, 1) — ось сохранена с размером 1

argmax по оси классов — это то, как классификатор превращает вероятности в ответ: класс с наибольшей вероятностью.

Broadcasting

Что будет, если сложить матрицу (3, 4) и вектор (4,)? NumPy «растянет» вектор на каждую строку, не копируя данные. Это называется broadcasting.

Правило: формы выравниваются по правому краю и сравниваются поразмерно. Две размерности совместимы, если они равны или одна из них равна 1 (недостающие слева считаются единицами). Размерность 1 растягивается до другой.

X (3, 4) X (3, 4) A[:, None] (3, 1) b (4,) c (3,) B[None, :] (1, 5) ────────────── ────────────── ─────────────────── итог (3, 4) ✓ 4 ≠ 3 ошибка ✗ итог (3, 5) ✓
import numpy as np

X = np.arange(12).reshape(3, 4).astype(float)
b = np.array([100, 200, 300, 400])
print(X + b)                          # b прибавлен к каждой строке

col_means = X.mean(axis=0)            # (4,)
print(X - col_means)                  # центрирование каждого столбца

row_sums = X.sum(axis=1, keepdims=True)   # (3, 1) — keepdims важен!
print(X / row_sums)                   # каждая строка в сумме даёт 1

a = np.array([1, 2, 3])
print(a[:, None] * a[None, :])        # (3,1) * (1,3) → таблица умножения 3×3
import numpy as np

X = np.ones((3, 4))
row_sums = X.sum(axis=1)       # форма (3,) — без keepdims
X / row_sums                   # (3, 4) и (3,): с конца 4 ≠ 3 → ошибка

Добавить ось

x[:, None] (то же, что x[:, np.newaxis] или x.reshape(-1, 1)) превращает вектор (n,) в столбец (n, 1). Это главный приём для broadcasting.

reshape и транспонирование

import numpy as np

a = np.arange(6)
print(a.reshape(2, 3))
print(a.reshape(3, -1))      # -1: «вычисли сам» → (3, 2)
M = a.reshape(2, 3)
print(M.T, M.T.shape)        # транспонирование: строки ↔ столбцы

imgs = np.zeros((10, 8, 8))  # 10 картинок 8×8
flat = imgs.reshape(10, -1)  # «распрямить» каждую в вектор из 64 чисел
print(flat.shape)

reshape не меняет порядок элементов — он по-новому «нарезает» ту же последовательность чисел. Количество элементов должно совпадать.

Графики

Модуль vis принимает массивы NumPy напрямую:

import numpy as np
import vis

x = np.linspace(-3, 3, 100)
vis.line(x, {"x²": x ** 2, "|x|": np.abs(x), "sin x": np.sin(x)}, title="Функции", xlabel="x")

rng = np.random.default_rng(0)
pts = rng.normal(size=(200, 2)) @ np.array([[1.0, 0.8], [0, 0.6]])
vis.scatter(pts, title="Облако точек", equal=True)

Итог

  • ndarray — массив чисел одного типа; главное его свойство — shape.
  • Операции поэлементные и векторизованные: пишите без циклов — это в десятки раз быстрее.
  • axis=k в агрегациях — ось, которая исчезает; keepdims=True сохраняет её с размером 1.
  • Broadcasting: формы сравниваются с конца, совместимы равные размерности и единицы.
  • reshape, .T, x[:, None] — инструменты, чтобы привести формы в соответствие.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

Какой будет форма (shape) результата np.zeros((3, 4)).sum(axis=0)?

Задание 2 Заполнить таблицу

Можно ли сложить массивы таких форм по правилам broadcasting?

можноошибка
(3, 4) и (4,)
(3, 4) и (3,)
(3, 4) и (3, 1)
(5, 1, 4) и (3, 1)
(2, 3) и (3, 2)
Отметьте ячейку — «+», пустая — «−».
Задание 3 Ввод ответа

Что выведет print(np.arange(12).reshape(3, 4)[1, 2])?

Задание 4 Программа на Python

Нормализация признаков. Дана матрица X формы (N, d): N объектов, d признаков. Напишите standardize(X), которая возвращает новую матрицу, где каждый столбец имеет среднее 0 и стандартное отклонение 1: (X - среднее столбца) / std столбца.

Без циклов — только операции NumPy и broadcasting.

Какие тесты проверяют код (3)
X = np.array([[1.0, 10], [2, 20], [3, 60]])
Z = standardize(X)
assert Z.shape == (3, 2), "Форма должна сохраниться"
assert np.allclose(Z.mean(axis=0), 0), "Среднее каждого столбца должно быть 0"
assert np.allclose(Z.std(axis=0), 1), "Стандартное отклонение каждого столбца должно быть 1"

import inspect
src = inspect.getsource(standardize)
assert "for " not in src and "while " not in src, "Решите без циклов"

rng = np.random.default_rng(1)
X = rng.normal(5, 3, size=(100, 4))
assert np.allclose(standardize(X), (X - X.mean(0)) / X.std(0))

Задание 5 Программа на Python

Попарные расстояния. Даны точки A формы (n, 2) и B формы (m, 2). Напишите pairwise_dist(A, B), возвращающую матрицу (n, m), где элемент [i, j] — евклидово расстояние между A[i] и B[j].

Подсказка: A[:, None, :] - B[None, :, :] имеет форму (n, m, 2). Циклы не используйте.

Какие тесты проверяют код (2)
A = np.array([[0.0, 0], [3, 4]])
B = np.array([[0.0, 0], [0, 4], [6, 8]])
D = pairwise_dist(A, B)
assert D.shape == (2, 3), f"Ожидалась форма (2, 3), получено {np.shape(D)}"
assert np.allclose(D, [[0, 4, 10], [5, 3, 5]])

import inspect
src = inspect.getsource(pairwise_dist)
assert "for " not in src and "while " not in src, "Решите без циклов, через broadcasting"

Войдите, чтобы отмечать пройденные уроки.