data · advanced · 8d
Числовой инструментарий
Собери небольшую библиотеку, на которую втайне опирается любая численная программа: векторы, матрицы, решатель линейных систем и описательную статистику — написанные тобой и проверенные на ответах, которые ты можешь подтвердить вручную. Здесь алгебра, которую ты выучил, перестаёт быть домашним заданием и становится кодом, который вызывает другой код. Ты на собственном опыте поймёшь, почему арифметика с плавающей точкой немного врёт, почему решить Ax = b сложнее, чем подсказывает учебник, и как доказать, что твои числа верны.
Результат
Самодостаточная числовая библиотека с типами вектор/матрица и арифметикой над ними, решателем Ax = b методом Гаусса и функциями mean/variance/std — всё это покрыто набором тестов, сверяющим результаты с вычисленными вручную и эталонными значениями, включая хотя бы один плохо обусловленный случай, который вскрывает численную ошибку.
Этапы
0/5 · 0%- 01Векторы и матрицы как данные
Прежде чем что-то решать, нужен способ хранить числа. Реши, как вектор и матрица живут в памяти — плоский массив со счётчиком строк или вложенные массивы, — и построй небольшие операции, на которых держится всё остальное: сложение, умножение на скаляр, скалярное произведение и умножение матрицы на вектор. Скалярное произведение — место, где новички впервые встречают тихий баг, ведь границы цикла и сопоставление индексов должны совпадать в точности; ошибись в одном — и ответ выглядит правдоподобно, но неверен. Держи операции чистыми: принимай входные данные, возвращай новый вектор или матрицу и никогда не мутируй аргументы, чтобы поздний шаг не испортил ранний. Награда за то, что этот слой получился скучным и правильным, — в том, что каждая более сложная вещь поверх него наследует эту правильность.
Критерии готовности- Сложение, масштабирование и скалярное произведение векторов дают верные результаты на проверенных вручную входах и отклоняют несовпадающие длины, а не выдают мусор.
- Умножение матрицы на вектор даёт результат, который ты можешь проверить вручную на примере 3x3, и операции возвращают новые объекты, а не мутируют входные данные.
- 02Реши Ax = b методом исключения
Теперь заставь инструментарий делать настоящую работу: решать систему линейных уравнений. Реализуй метод Гаусса — приведи матрицу к верхнетреугольному виду, вычитая масштабированные строки, а затем обратной подстановкой считай неизвестные. Это та же процедура, что ты делал вручную в алгебре, но машина заставляет быть точным в порядке операций и в учёте того, какая строка какую масштабирует. Начни с наивной версии на «удобной» системе 3x3, где ответ уже известен, чтобы неверный результат был очевидно неверным. Ты обнаружишь, что у алгоритма стоимость порядка n в кубе операций — вот почему решение системы из тысячи уравнений это реальные затраты, а не бесплатно: сложность, которую ты изучал, перестаёт быть абстракцией в тот момент, когда твой решатель начинает тормозить.
Критерии готовности- Решатель возвращает верный x для системы 3x3, решение которой ты вычислил вручную.
- Подстановка возвращённого x обратно в Ax воспроизводит b в пределах малого допуска, подтверждая решение, а не предполагая его.
- 03Заставь решатель пережить плохие строки
Наивный решатель ломается в первый же раз, когда ведущий элемент равен нулю — ты делишь на него и получаешь бесконечность или NaN, — и тихо теряет точность, когда ведущий элемент просто очень мал. Лекарство — частичный выбор ведущего элемента: перед исключением столбца переставь наверх строку с наибольшим по модулю значением в этом столбце, чтобы всегда делить на самое большое доступное число. Построй небольшой пример, на котором наивная версия выдаёт дико неверный ответ, а затем посмотри, как выбор ведущего элемента её спасает; этот контраст и есть весь урок. Это твоя первая настоящая встреча с мыслью, что математически верный алгоритм всё равно может быть численно неверным и что числа с плавающей точкой на числовой прямой — не те точные вещественные числа, которыми ты рассуждал на бумаге.
Критерии готовности- Система, ставящая ноль или крошечное значение на диагональ, больше не падает и не возвращает NaN — выбор ведущего элемента переставляет строки и даёт верный ответ.
- Тест демонстрирует конкретный случай, где наивный решатель ошибается, а решатель с выбором ведущего элемента прав, бок о бок.
- 04Опиши столбец чисел
Добавь статистическую сторону: по вектору выборки вычисли среднее, дисперсию и стандартное отклонение. Среднее — просто; дисперсия — место, где ждёт тонкая ловушка. Учебная формула, вычитающая среднее квадратов из квадрата среднего, делается в один проход по плавающей точке, но катастрофически теряет значимость, когда числа большие и близкие, возвращая отрицательную дисперсию, что математически невозможно. Реализуй вместо неё численно честный двухпроходный вариант (или бегущую формулу Уэлфорда) и докажи тестом на больших почти равных значениях, что наивная формула проваливается, а твоя держится. Осознанно выбери между дисперсией генеральной совокупности и выборочной — выбор «делить на n» против «делить на n минус один» не косметический, и сеньор-инженер заявляет, какой из них он поставляет и почему.
Критерии готовности- Среднее, дисперсия и стандартное отклонение совпадают с эталоном (например, numpy или ручной счёт) на небольшом известном наборе данных.
- Тест на больших почти равных значениях показывает, что наивная однопроходная дисперсия проваливается (например, уходит в минус), тогда как твоя реализация остаётся корректной и неотрицательной.
- 05Проверь по известной истине
Числовая библиотека, которой нельзя доверять, ничего не стоит, поэтому этот этап — про то, как доверие заслужить. Построй набор тестов вокруг ответов, которые ты можешь проверить независимо: решений, вычисленных вручную; тождеств, которые обязаны выполняться (матрица, умноженная на своё решение, воспроизводит правую часть); и эталонной реализации, с которой ты сверяешься. Дисциплина здесь — утверждать с допуском, а не на точное равенство: результаты с плавающей точкой почти никогда не совпадают побитово, поэтому ты сравниваешь в пределах эпсилон и выбираешь этот эпсилон осознанно. Покрой скучный счастливый путь, но основную энергию потрать на края: вырожденную матрицу без единственного решения, пустой ввод, вектор из одного элемента. Тесты — не довесок; они единственное, что стоит между «выглядит верно» и «оно верно».
Критерии готовности- У каждой публичной операции есть хотя бы один тест, проверяющий её против проверяемого эталона в пределах выбранного допуска, а не на точное равенство.
- Граничные случаи — вырожденная матрица, пустой вектор, несовпадающие размерности — протестированы и дают понятную ошибку или документированный результат вместо тихой бессмыслицы.
Стартер
- README.md
- src/numeric.ts
- test/numeric.test.ts
Распакуй, реализуй заглушки, затем гоняй тесты, пока не позеленеют: bun test
Рубрика
| Джуниор | Миддл | Сеньор | |
|---|---|---|---|
| Осведомлённость об ошибках плавающей точки | Знает, что результаты с плавающей точкой неточны, и использует допуск в утверждениях вместо строгого равенства. | Указывает конкретную операцию, вызывающую катастрофическую потерю значимости в наивной дисперсии (вычитание двух больших почти равных чисел), реализует двухпроходную формулу или формулу Уэлфорда для её устранения и доказывает исправление тестом на больших почти равных значениях, где наивная формула даёт отрицательную дисперсию. | Умеет поместить ошибку наивной формулы дисперсии в контекст относительного эпсилон машины: для значений порядка M абсолютная ошибка вычитания ~M * epsilon, что делает формулу корректной только при дисперсии >> M^2 * epsilon. Умеет сформулировать, когда суммирование Кахана дополнительно уменьшит накопленную ошибку в скалярных произведениях или нарастающих суммах. |
| Выбор ведущего элемента и численная устойчивость | Реализует метод Гаусса корректно для хорошо обусловленных квадратных систем без нулевых ведущих элементов; обратная подстановка даёт верный x. | Добавляет частичный выбор ведущего элемента (перед каждым шагом исключения переставляет строку с наибольшим по модулю значением в текущем столбце) и строит тест, в котором наивная версия даёт NaN или грубо неверный результат, а версия с выбором ведущего элемента — точный. | Объясняет, почему частичный выбор ведущего элемента ограничивает фактор роста до 2^(n-1) в худшем случае (что теоретически позволяет построить патологическую матрицу) и почему на практике это редко проблема; умеет описать число обусловленности как отношение максимального к минимальному сингулярному значению и сформулировать, что число обусловленности 1e12 означает достоверность лишь ~4 значащих цифр решения в double precision. |
| Обработка граничных случаев и покрытие тестами | Тестирует счастливый путь: известная система 3x3 с чистым целочисленным решением, среднее небольших целых чисел, скалярное произведение коротких векторов. | Покрывает канонические режимы отказа: вырожденная матрица (обнаружена и сообщена, а не тихо NaN), несовпадающие длины векторов (отклонены с понятной ошибкой), пустой ввод, вектор из одного элемента и плохо обусловленная почти вырожденная система, которая вскрывает потери плавающей точки. | Выбирает допуски осознанно: эпсилон для проверки невязки (||Ax - b|| / ||b|| < 1e-10) отличается от эпсилон для проверки неотрицательности дисперсии; умеет объяснить, почему относительный допуск почти всегда верен, а абсолютный — почти всегда источник ошибок. Документирует, какие операции возвращают NaN, а какие бросают исключение, и почему контракт намеренный. |
Эталонный разбор (спойлер)
Катастрофическая потеря значимости в наивной дисперсии: учебная формула E[X^2] - E[X]^2 вычитает два числа, близкие по величине, когда дисперсия мала относительно среднего. В IEEE 754 double precision значения порядка M имеют абсолютную ошибку ~M * 2^-52 (~2,2e-16). Если дисперсия меньше M^2 * epsilon, вычитание возвращает ноль или отрицательное число. Бегущая формула Уэлфорда полностью избегает этого, накапливая отклонения от текущего среднего.
Почему частичный выбор ведущего элемента: без него деление на малый (близкий к нулю) ведущий элемент усиливает ошибку округления на всех последующих шагах исключения. Частичный выбор всегда делит на наибольшее доступное число в столбце, ограничивая усиление. Режим отказа без выбора ведущего: система 2x2 [[0.0001, 1], [1, 1]] даёт грубо неверный ответ при наивном исключении, но верный ответ после перестановки строк.
Дисперсия генеральной совокупности против выборочной: деление на n даёт оценку максимального правдоподобия дисперсии генеральной совокупности (смещённую для малых выборок); деление на n-1 даёт поправку Бесселя — несмещённую оценку дисперсии по выборке. Для задач анализа данных при n > 30 разница пренебрежимо мала; для небольших статистических тестов (A/B при n=10) она важна, и выбор должен быть явным в API.
Проверка невязки как прокси корректности: после решения Ax = b вычисли r = Ax - b и проверь ||r|| / ||b|| < epsilon. Это не доказывает точность решения — плохо обусловленная матрица может иметь малую невязку при большой ошибке, — но ловит грубые баги (неверный порядок обратной подстановки, ошибка на единицу в исключении). Для продакшен-решателя проверка невязки плюс оценка числа обусловленности даёт полную картину.
Сделай по-сеньорски
- Замени повторное исключение LU-разложением: разложи A один раз на нижне- и верхнетреугольную пару, а затем дёшево решай множество разных правых частей, переиспользуя множители, — стандартный приём, когда ты решаешь Ax = b для одной и той же A снова и снова.
- Добавь осведомлённость о числе обусловленности: оцени, насколько твоя матрица усиливает входную ошибку, и сигнализируй, когда система плохо обусловлена, чтобы вызывающий узнал «этот ответ хрупок» вместо доверия к цифрам, которые суть чистый шум плавающей точки.
- Прогони бенчмарк решателя по мере роста размера системы и подтверди, что время работы действительно масштабируется как n в кубе, превратив изученный класс сложности в кривую, которую ты измерил сам.