Как проверять корректность решения на NumPy, а не только успешное выполнение кода?
Корректность NumPy-решения проверяется через np.testing, property-based тесты (Hypothesis), сравнение с эталонной реализацией, проверку dtype/shape и граничные случаи (NaN, inf, overflow).
Уровни верификации
Успешное выполнение кода означает лишь отсутствие исключения. Корректность требует отдельной проверки по четырём осям: значения, форма, тип данных, память/побочные эффекты.
np.testing — основной инструмент
Модуль numpy.testing предоставляет функции, учитывающие floating-point семантику:
import numpy as np
import numpy.testing as npt
# assert_allclose: относительная/абсолютная погрешность
npt.assert_allclose(actual, desired, rtol=1e-5, atol=1e-8,
err_msg="softmax sum != 1")
# assert_array_equal: побитовое равенство (для int-логики)
npt.assert_array_equal(indices, expected_indices)
# assert_array_less: монотонность, ранги
npt.assert_array_less(0, probabilities) # все > 0
# Проверка dtype
assert result.dtype == np.float32, f"got {result.dtype}"
# Проверка shape
assert result.shape == (batch_size, num_classes), result.shape
# Проверка отсутствия NaN/inf
assert np.all(np.isfinite(result)), "result contains NaN or inf"
Эталонная реализация (oracle testing)
Для нетривиальных вычислений пишут медленную, очевидно правильную реализацию на Python-циклах и сравнивают с векторизованной:
def softmax_reference(x):
"""Медленная, но очевидно корректная реализация."""
result = np.empty_like(x, dtype=np.float64)
for i in range(len(x)):
e = np.exp(x[i] - x[i].max()) # численная стабильность
result[i] = e / e.sum()
return result
def softmax_fast(x):
shifted = x - x.max(axis=1, keepdims=True)
e = np.exp(shifted)
return e / e.sum(axis=1, keepdims=True)
rng = np.random.default_rng(42)
X = rng.standard_normal((100, 10)).astype(np.float32)
npt.assert_allclose(
softmax_fast(X),
softmax_reference(X),
rtol=1e-4 # float32 точность
)
Property-based тестирование с Hypothesis
Библиотека Hypothesis генерирует граничные случаи автоматически — нули, NaN, inf, переполнение, пустые массивы:
from hypothesis import given, settings
from hypothesis.extra.numpy import arrays
import hypothesis.strategies as st
@given(arrays(np.float32, shape=st.tuples(st.integers(1,50), st.integers(1,50)),
elements=st.floats(min_value=-100, max_value=100,
allow_nan=False, allow_infinity=False)))
@settings(max_examples=200)
def test_softmax_sums_to_one(X):
out = softmax_fast(X)
npt.assert_allclose(out.sum(axis=1), np.ones(len(X)), atol=1e-5)
assert np.all(out >= 0)
Проверка инвариантов и граничных случаев вручную
- NaN propagation:
np.nan_to_numмаскирует проблему — лучше проверять черезnp.isnan(result).any()явно. - Числовое переполнение: тест с
x = np.array([1000.0], dtype=np.float32)выявляет inf в exp(). - Пустой массив:
softmax_fast(np.empty((0, 5)))не должен падать. - View vs copy: если функция модифицирует вход in-place, проверить
np.shares_memory(input, output).
Мониторинг в production
В пайплайнах, работающих на реальных данных, добавляют runtime-ассерты: диапазоны входов, распределение выходов (Prometheus-метрики model_output_mean, model_output_std), долю NaN/inf. Это обнаруживает data drift раньше, чем деградируют бизнес-метрики.
Подводные камни
- Только assert без tolerance:
assert a == bдля float всегда сравнивает побитово; нужноassert_allclose. - Фиксированный тестовый seed без edge cases:
np.random.seed(42)не генерирует нули, NaN, inf — нужны отдельные тесты граничных значений. - Тест только на маленьких данных: переполнение и потеря точности float32 проявляются при больших значениях или большом числе элементов.
- Игнорирование dtype в assert: алгоритм может давать правильные числа, но возвращать float64 вместо float32, что удваивает память в production.
- Нет теста на in-place мутацию: функция, незаметно меняющая входной массив, ломает кеши и upstream-данные.
- Comparison с Python float:
result == 0.1всегда False из-за float representation; никогда не сравнивайте float на точное равенство. - Нет теста на пустой или одноэлементный массив: частые источники деления на ноль или неверного axis-reduce.
- Отсутствие smoke-теста на реальных данных: синтетические тесты не всегда воспроизводят distribution shift, который возникнет в production.
What hurts your answer
- Сразу обвинять NumPy, не проверив соседние слои системы
- Чинить симптом без минимального воспроизведения и evidence
- Не учитывать версии, конфигурацию, окружение и recent changes
What they're listening for
- Умеет локализовать проблему вокруг NumPy
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения