NumPy: массивы, векторизация и broadcasting
Зачем 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 растягивается до другой.
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
Какой будет форма (shape) результата np.zeros((3, 4)).sum(axis=0)?
Можно ли сложить массивы таких форм по правилам broadcasting?
| можно | ошибка | |
|---|---|---|
(3, 4) и (4,) |
||
(3, 4) и (3,) |
||
(3, 4) и (3, 1) |
||
(5, 1, 4) и (3, 1) |
||
(2, 3) и (3, 2) |
Что выведет print(np.arange(12).reshape(3, 4)[1, 2])?
Нормализация признаков. Дана матрица 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))
import numpy as np
def standardize(X):
return (X - X.mean(axis=0)) / X.std(axis=0)
Попарные расстояния. Даны точки 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"
import numpy as np
def pairwise_dist(A, B):
diff = A[:, None, :] - B[None, :, :]
return np.sqrt((diff ** 2).sum(axis=-1))
Войдите, чтобы отмечать пройденные уроки.