29. (Л) Базова лінійна алгебра у NumPy. Матричні операції, системи лінійних рівнянь¶
Зміст лекції¶
- Навіщо це програмісту
- Вектор і скалярний добуток
- Норма вектора та відстань
- Матриця: форма і транспонування
*і@— дві різні операції- Матричний добуток: правило розмірів
- Матриця на вектор
- Одинична матриця
- Визначник
- Обернена матриця
- Системи лінійних рівнянь
solveпротиinv- Коли розв'язку немає: вироджена матриця
- Ранг матриці
- Приклад: розрахунок суміші
- Перевизначені системи та
lstsq - Власні числа: коротко
- Типові помилки
- Підсумок
Навіщо це програмісту¶
Лінійна алгебра — не окрема «математична» тема, а мова, якою описують багато прикладних задач:
- комп'ютерна графіка: поворот, масштабування, зсув — усе це множення координат на матрицю;
- обробка зображень: фільтри, перетворення кольорових просторів;
- машинне навчання: один шар нейромережі — це
inputs @ weights + bias; - економіка й інженерія: система рівнянь на кшталт «скільки чого змішати, щоб отримати заданий склад»;
- геометрія й фізика: відстані, кути, проєкції.
NumPy покриває цю тему модулем numpy.linalg (від linear algebra) плюс оператором @ для множення матриць. Далі — базовий набір, якого вистачає для більшості прикладних задач.
Вектор і скалярний добуток¶
Вектор у NumPy — звичайний одновимірний масив. Скалярний добуток (dot product) двох векторів однакової довжини — це сума попарних добутків:
import numpy as np
prices = np.array([25.0, 40.0, 15.0, 8.0])
amounts = np.array([2.0, 1.0, 3.0, 10.0])
# три записи однієї операції
print(np.sum(prices * amounts))
print(np.dot(prices, amounts))
print(prices @ amounts)
Усі три рядки дають те саме: загальну вартість покупки. Різниця в тому, що prices * amounts спершу створює проміжний масив із чотирьох чисел, а @ і np.dot рахують суму одразу, без зайвої пам'яті.
Оператор @
@ — це окремий оператор Python для матричного множення (PEP 465). Для двох одновимірних масивів він дає число, для двовимірних — добуток матриць. Саме він, а не *, відповідає множенню з підручника лінійної алгебри.
Довжини мають збігатися:
import numpy as np
first = np.array([1.0, 2.0, 3.0])
second = np.array([1.0, 2.0])
try:
first @ second
except ValueError as error:
print("ValueError:", error)
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 3)
Норма вектора та відстань¶
Норма — довжина вектора, корінь із суми квадратів координат. Рахує її np.linalg.norm:
import numpy as np
point = np.array([3.0, 4.0])
print(np.linalg.norm(point))
print(np.sqrt(np.sum(point ** 2)))
first = np.array([1.0, 2.0, 3.0])
second = np.array([4.0, 6.0, 3.0])
# евклідова відстань між двома точками
print(np.linalg.norm(first - second))
Норма працює і з набором точок одразу — треба лише вказати вісь:
import numpy as np
# рядок — точка, стовпці — координати x, y
points = np.array([
[0.0, 0.0],
[3.0, 4.0],
[6.0, 8.0],
[1.0, 1.0],
])
target = np.array([3.0, 4.0])
distances = np.linalg.norm(points - target, axis=1)
print(np.round(distances, 3))
print("nearest index:", distances.argsort()[1])
Тут працює broadcasting із минулої лекції: (4, 2) - (2,) віднімає target від кожного рядка, а axis=1 каже рахувати норму по кожному рядку окремо. Одна з найчастіших задач — «знайти найближчий об'єкт» — вирішується двома рядками.
Кут між векторами
Через скалярний добуток і норми рахується косинус кута: a @ b / (norm(a) * norm(b)). Ця формула — основа «схожості» текстів і рекомендаційних систем.
Матриця: форма і транспонування¶
Матриця — двовимірний масив. Її форма записується як (рядки, стовпці).
Транспонування міняє рядки і стовпці місцями: елемент a[i, j] стає a[j, i]. Атрибут .T:
import numpy as np
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
])
print(matrix)
print(matrix.shape)
print(matrix.T)
print(matrix.T.shape)
матриця (2, 3) транспонована (3, 2)
+---+---+---+ +---+---+
| 1 | 2 | 3 | | 1 | 4 |
+---+---+---+ .T +---+---+
| 4 | 5 | 6 | -> | 2 | 5 |
+---+---+---+ +---+---+
| 3 | 6 |
+---+---+
.T повертає перегляд (view), а не копію — даних у пам'яті не додається:
import numpy as np
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
])
transposed = matrix.T
transposed[0, 0] = 99
print(matrix)
print(transposed.base is matrix)
Транспонування одновимірного масиву нічого не робить
У масиву форми (3,) немає другої осі, тому .T повертає його ж. Щоб отримати «стовпець», потрібен reshape(-1, 1) або [:, np.newaxis].
import numpy as np
vector = np.array([1, 2, 3])
print(vector.shape, vector.T.shape)
print(vector[:, np.newaxis])
print(vector[:, np.newaxis].shape)
* і @ — дві різні операції¶
Це найчастіше джерело помилок у новачків. * — поелементне множення (те саме, що + чи -), @ — матричне.
import numpy as np
a = np.array([
[1, 2],
[3, 4],
])
b = np.array([
[5, 6],
[7, 8],
])
print(a * b)
print(a @ b)
Поелементно: 1*5 = 5, 2*6 = 12, і так далі. Матрично — кожен елемент результату є скалярним добутком рядка першої матриці на стовпець другої:
Квадратні матриці приховують помилку
Для матриць 2x2 обидві операції проходять без помилки — просто дають різні числа. Якщо переплутати * і @ на квадратних даних, програма не впаде, а тихо порахує не те.
Матричний добуток: правило розмірів¶
Щоб перемножити A @ B, внутрішні розміри мають збігатися:
import numpy as np
a = np.arange(6).reshape(2, 3)
b = np.arange(12).reshape(3, 4)
result = a @ b
print(a.shape, "@", b.shape, "->", result.shape)
print(result)
try:
b @ a
except ValueError as error:
print("ValueError:", error)
(2, 3) @ (3, 4) -> (2, 4)
[[20 23 26 29]
[56 68 80 92]]
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 4)
Звідси головна властивість: матричний добуток не комутативний, A @ B і B @ A — різні речі (а часто друге взагалі не існує).
import numpy as np
a = np.array([
[1, 2],
[0, 1],
])
b = np.array([
[1, 0],
[3, 1],
])
print(a @ b)
print(b @ a)
print(np.array_equal(a @ b, b @ a))
Ще одна корисна властивість — транспонування добутку перевертає порядок: \((AB)^T = B^T A^T\).
import numpy as np
a = np.arange(6).reshape(2, 3)
b = np.arange(12).reshape(3, 4)
print(np.array_equal((a @ b).T, b.T @ a.T))
Матриця на вектор¶
Найчастіший випадок на практиці: матриця (m, n) множиться на вектор (n,) і дає вектор (m,). NumPy розуміє одновимірний масив як стовпець і повертає результат теж одновимірним.
import numpy as np
# рядок — рецепт напою, стовпець — інгредієнт
recipes = np.array([
[50.0, 30.0, 20.0],
[0.0, 80.0, 20.0],
[70.0, 0.0, 30.0],
])
# ціна за одиницю кожного інгредієнта
prices = np.array([0.4, 0.25, 0.1])
cost = recipes @ prices
print(np.round(cost, 2))
print(recipes.shape, "@", prices.shape, "->", cost.shape)
Той самий результат через явні цикли зайняв би вісім рядків і працював би в десятки разів повільніше.
Класичний приклад із графіки — поворот точки навколо початку координат на кут \(\theta\):
import numpy as np
angle = np.pi / 2 # 90 градусів у радіанах
rotation = np.array([
[np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)],
])
point = np.array([1.0, 0.0])
print(np.round(rotation @ point, 6))
# кілька точок одразу: (n, 2) @ (2, 2).T
points = np.array([
[1.0, 0.0],
[0.0, 1.0],
[2.0, 3.0],
])
rotated = points @ rotation.T
print(np.round(rotated, 6))
Чому points @ rotation.T
Коли точки лежать у рядках масиву (n, 2), формула повороту перетворюється на points @ R.T. Якби точки були у стовпцях ((2, n)), писали б R @ points. Обидва варіанти зустрічаються в коді — дивіться на shape.
Одинична матриця¶
Одинична матриця має одиниці на головній діагоналі й нулі поза нею. Вона грає роль числа 1: A @ I == A.
import numpy as np
identity = np.eye(3)
print(identity)
a = np.array([
[2.0, 1.0, 0.0],
[0.0, 3.0, 5.0],
[1.0, 1.0, 1.0],
])
print(np.array_equal(a @ identity, a))
print(np.array_equal(identity @ a, a))
Поряд корисні np.diag (дістати діагональ або побудувати діагональну матрицю) і np.trace (сума діагоналі):
import numpy as np
a = np.array([
[2.0, 1.0, 0.0],
[0.0, 3.0, 5.0],
[1.0, 1.0, 1.0],
])
print(np.diag(a))
print(np.trace(a))
print(np.diag(np.array([1.0, 2.0, 3.0])))
Визначник¶
Визначник (determinant) — число, яке показує, чи «вироджена» квадратна матриця. Для розміру 2x2 формула проста:
$$ \det \begin{pmatrix} a & b \ c & d \end{pmatrix} = ad - bc $$ Якщо він дорівнює нулю, матриця необоротна: рядки лінійно залежні, і перетворення «склеює» простір.
import numpy as np
good = np.array([
[2.0, 1.0],
[1.0, 3.0],
])
# другий рядок = перший, помножений на 2
bad = np.array([
[2.0, 1.0],
[4.0, 2.0],
])
print(np.linalg.det(good))
print(np.linalg.det(bad))
Зверніть увагу на 5.000000000000001: визначник рахується чисельно, тому порівнювати його з нулем через == не можна. Для перевірки беруть невеликий поріг або, надійніше, np.linalg.matrix_rank (про неї нижче).
import numpy as np
matrix = np.array([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
])
determinant = np.linalg.det(matrix)
print(determinant)
print(determinant == 0)
print(abs(determinant) < 1e-10)
Математично визначник цієї матриці дорівнює нулю (третій рядок = другий плюс різниця рядків), але чисельно вийшло 6.7e-16. Перевірка == 0 дала False — тобто «матриця нормальна», хоча вона вироджена. Порівняння з порогом дає правильну відповідь.
Обернена матриця¶
Обернена матриця \(A^{-1}\) — це така, що \(A A^{-1} = A^{-1} A = I\). Рахує її np.linalg.inv.
import numpy as np
a = np.array([
[2.0, 1.0],
[1.0, 3.0],
])
inverse = np.linalg.inv(a)
print(np.round(inverse, 4))
print(np.round(a @ inverse, 10))
print(np.allclose(a @ inverse, np.eye(2)))
np.allclose замість ==
Обчислення з плаваючою комою майже ніколи не дають точний результат. np.allclose(x, y) порівнює з допуском і є стандартним способом перевірки в лінійній алгебрі.
Для виродженої матриці inv кидає помилку:
import numpy as np
bad = np.array([
[2.0, 1.0],
[4.0, 2.0],
])
try:
np.linalg.inv(bad)
except np.linalg.LinAlgError as error:
print("LinAlgError:", error)
np.linalg.LinAlgError — окремий клас винятку для всього модуля linalg. Його й ловлять, коли дані можуть виявитися виродженими.
Системи лінійних рівнянь¶
Ось навіщо все попереднє. Система
у матричному вигляді — це \(Ax = b\), де
Розв'язок дає np.linalg.solve(A, b):
import numpy as np
a = np.array([
[2.0, 1.0],
[1.0, 3.0],
])
b = np.array([11.0, 18.0])
solution = np.linalg.solve(a, b)
print(solution)
# перевірка: підставляємо розв'язок назад
print(a @ solution)
print(np.allclose(a @ solution, b))
Відповідь: \(x = 3\), \(y = 5\). Перевірка підстановкою — обов'язковий крок, він коштує один рядок.
Розмір системи не обмежений двома рівняннями. Три невідомі:
import numpy as np
a = np.array([
[1.0, 2.0, 3.0],
[2.0, -1.0, 1.0],
[3.0, 1.0, -1.0],
])
b = np.array([14.0, 3.0, 2.0])
solution = np.linalg.solve(a, b)
print(np.round(solution, 6))
print(np.allclose(a @ solution, b))
solve вимагає, щоб A була квадратною і невиродженою, а довжина b дорівнювала кількості рядків A.
solve проти inv¶
Математично \(x = A^{-1} b\), тому виникає спокуса написати np.linalg.inv(a) @ b. Так робити не варто: solve точніший і швидший, бо не рахує обернену матрицю повністю.
import numpy as np
rng = np.random.default_rng(0)
size = 500
a = rng.random((size, size)) + np.eye(size) * size
b = rng.random(size)
by_solve = np.linalg.solve(a, b)
by_inverse = np.linalg.inv(a) @ b
print(np.allclose(by_solve, by_inverse))
print("max difference:", np.max(np.abs(by_solve - by_inverse)))
На цій системі різниця мікроскопічна, але на погано обумовлених матрицях inv втрачає точність помітно. Порівняймо швидкість (абсолютні числа залежать від комп'ютера, важливе співвідношення):
import time
import numpy as np
rng = np.random.default_rng(0)
size = 800
a = rng.random((size, size)) + np.eye(size) * size
b = rng.random(size)
start = time.perf_counter()
for _ in range(10):
np.linalg.solve(a, b)
solve_time = time.perf_counter() - start
start = time.perf_counter()
for _ in range(10):
np.linalg.inv(a) @ b
inverse_time = time.perf_counter() - start
print(f"solve: {solve_time:.4f} s")
print(f"inv: {inverse_time:.4f} s")
print(f"ratio: {inverse_time / solve_time:.1f}x")
Правило
Потрібен розв'язок системи — np.linalg.solve. Обернена матриця потрібна рідко й сама по собі: наприклад, коли її треба показати або застосовувати багато разів до різних правих частин (та й тоді краще передати solve одразу матрицю правих частин).
solve вміє розв'язувати кілька систем із тією самою матрицею за один виклик — треба лише передати b як матрицю (n, k):
import numpy as np
a = np.array([
[2.0, 1.0],
[1.0, 3.0],
])
# два різних набори правих частин у стовпцях
b = np.array([
[11.0, 5.0],
[18.0, 10.0],
])
solution = np.linalg.solve(a, b)
print(solution)
Кожен стовпець результату — розв'язок для відповідного стовпця b.
Коли розв'язку немає: вироджена матриця¶
Якщо рівняння лінійно залежні (одне є комбінацією інших), система або не має розв'язку, або має їх безліч. solve у такому разі кидає LinAlgError:
import numpy as np
# друге рівняння — перше, помножене на 2
a = np.array([
[1.0, 2.0],
[2.0, 4.0],
])
b = np.array([5.0, 11.0])
try:
np.linalg.solve(a, b)
except np.linalg.LinAlgError as error:
print("LinAlgError:", error)
print("det:", np.linalg.det(a))
print("rank:", np.linalg.matrix_rank(a))
У реальній програмі це обробляють так:
import numpy as np
def solve_system(matrix, right_side):
"""Повертає розв'язок системи або None, якщо матриця вироджена."""
try:
return np.linalg.solve(matrix, right_side)
except np.linalg.LinAlgError:
return None
good = np.array([
[2.0, 1.0],
[1.0, 3.0],
])
bad = np.array([
[1.0, 2.0],
[2.0, 4.0],
])
right_side = np.array([11.0, 18.0])
print(solve_system(good, right_side))
print(solve_system(bad, right_side))
Майже вироджена матриця небезпечніша за вироджену
Якщо визначник не нуль, а 1e-15, solve не кине помилку — він поверне числа, які можуть не мати сенсу. Перевірити стійкість системи допомагає число обумовленості np.linalg.cond: чим воно більше, тим менше довіри до результату.
import numpy as np
stable = np.array([
[2.0, 1.0],
[1.0, 3.0],
])
shaky = np.array([
[1.0, 2.0],
[2.0, 4.000001],
])
print(f"stable cond: {np.linalg.cond(stable):.2f}")
print(f"shaky cond: {np.linalg.cond(shaky):.2e}")
Орієнтир: cond порядку одиниць чи десятків — добре; 1e10 і більше — результату довіряти не можна, дані треба переглянути.
Ранг матриці¶
Ранг — кількість лінійно незалежних рядків. Для квадратної матриці «ранг дорівнює розміру» означає те саме, що «визначник не нуль», але matrix_rank стійкіша до похибок округлення і працює з будь-якою формою.
import numpy as np
full = np.array([
[1.0, 2.0],
[3.0, 5.0],
])
# третій рядок = перший + другий
dependent = np.array([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[5.0, 7.0, 9.0],
])
print(np.linalg.matrix_rank(full), full.shape)
print(np.linalg.matrix_rank(dependent), dependent.shape)
Ранг 2 при розмірі 3 означає: одне рівняння зайве, незалежної інформації лише на два. Перевірка рангу перед solve — спосіб зрозуміти, чи взагалі має сенс розв'язувати систему.
Приклад: розрахунок суміші¶
Типова прикладна задача. Є три сировини з відомим вмістом трьох компонентів, треба отримати 100 кг суміші із заданим складом.
| Сировина | Білок, % | Жир, % | Волокно, % |
|---|---|---|---|
| A | 40 | 10 | 20 |
| B | 20 | 30 | 30 |
| C | 10 | 5 | 60 |
Ціль: 100 кг суміші, у яких 21.5 кг білка, 12.75 кг жиру і 40.5 кг волокна.
Якщо взяти \(x_A\), \(x_B\), \(x_C\) кілограмів сировини, то для кожного компонента отримуємо своє рівняння:
$$ \begin{cases} 0.40 x_A + 0.20 x_B + 0.10 x_C = 21.5 \ 0.10 x_A + 0.30 x_B + 0.05 x_C = 12.75 \ 0.20 x_A + 0.30 x_B + 0.60 x_C = 40.5 \end{cases} $$ Отримуємо систему з трьох рівнянь — матриця складів множиться на вектор кількостей.
import numpy as np
# рядок — компонент, стовпець — сировина
composition = np.array([
[0.40, 0.20, 0.10], # білок
[0.10, 0.30, 0.05], # жир
[0.20, 0.30, 0.60], # волокно
])
target = np.array([21.5, 12.75, 40.5])
materials = np.array(["A", "B", "C"])
amounts = np.linalg.solve(composition, target)
for name, value in zip(materials, amounts):
print(f"{name}: {value:.2f} kg")
print("total:", round(amounts.sum(), 2), "kg")
print("check:", np.allclose(composition @ amounts, target))
Окремо варто перевіряти, чи має відповідь фізичний сенс: від'ємна кількість сировини математично коректна, а практично — ні.
import numpy as np
composition = np.array([
[0.40, 0.20, 0.10],
[0.10, 0.30, 0.05],
[0.20, 0.30, 0.60],
])
# недосяжний склад: забагато жиру
target = np.array([15.0, 29.5, 44.0])
amounts = np.linalg.solve(composition, target)
print(np.round(amounts, 2))
if np.any(amounts < 0):
print("mixture is not feasible: negative amount")
Перевизначені системи та lstsq¶
Часто рівнянь більше, ніж невідомих: наприклад, є 10 вимірювань і 2 параметри прямої. Точного розв'язку зазвичай не існує, але можна знайти такий, що дає найменшу суму квадратів відхилень. Це робить np.linalg.lstsq (least squares — метод найменших квадратів).
Підберемо пряму \(y = kx + c\) до набору точок, мінімізуючи \(\sum_i (y_i - kx_i - c)^2\):
import numpy as np
hours = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
score = np.array([52.0, 55.0, 61.0, 64.0, 70.0, 72.0])
# матриця плану: стовпець x і стовпець одиниць
design = np.column_stack([hours, np.ones(hours.size)])
print(design)
result = np.linalg.lstsq(design, score, rcond=None)
slope, intercept = result[0]
print(f"slope: {slope:.3f}")
print(f"intercept: {intercept:.3f}")
print("predicted for 7 hours:", round(slope * 7 + intercept, 2))
[[1. 1.]
[2. 1.]
[3. 1.]
[4. 1.]
[5. 1.]
[6. 1.]]
slope: 4.229
intercept: 47.533
predicted for 7 hours: 77.13
lstsq повертає кортеж із чотирьох елементів: розв'язок, суму квадратів залишків, ранг матриці та її сингулярні числа. У більшості випадків потрібен лише перший.
lstsq не падає на вироджених даних — на відміну від solve, він завжди щось повертає, тому корисний у прикладних задачах апроксимації.
Власні числа: коротко¶
np.linalg.eig знаходить власні числа і власні вектори: такі вектори, напрям яких матриця не змінює, а лише розтягує в \(\lambda\) разів: \(Av = \lambda v\). Тема виходить за межі курсу, але знати про функцію варто — вона лежить в основі методу головних компонент (PCA) та аналізу стійкості систем.
import numpy as np
matrix = np.array([
[2.0, 0.0],
[0.0, 3.0],
])
values, vectors = np.linalg.eig(matrix)
print(values)
print(vectors)
Для діагональної матриці власні числа — це і є діагональ, а власні вектори — осі координат.
Типові помилки¶
* замість @. Найчастіша помилка. a * b множить поелементно; для квадратних матриць помилки не буде, буде неправильний результат.
Переплутаний порядок множників. A @ B ≠ B @ A. Перед множенням подумайте, які розміри мають зійтися.
Не збігаються внутрішні розміри. (2, 3) @ (2, 3) — помилка; потрібне (2, 3) @ (3, k). Часто лікується транспонуванням: a @ b.T.
.T на одновимірному масиві. Нічого не змінює. Для стовпця — reshape(-1, 1).
Порівняння визначника з нулем через ==. Через похибки округлення нуль майже ніколи не буває точним. Беріть поріг або matrix_rank.
inv(a) @ b замість solve(a, b). Повільніше і менш точно.
Не перевірений розв'язок. np.allclose(a @ x, b) — один рядок, який рятує від довгого налагодження.
Цілі типи там, де потрібні дробові. Якщо матриця має dtype=int, solve сам переведе результат у float, але всі попередні обчислення (наприклад, ділення) можуть мовчки відкинути дробову частину. Для лінійної алгебри одразу створюйте масиви з float.
Незловлений LinAlgError. Якщо матриця приходить ззовні (введення користувача, файл), виклик solve має бути в try/except.
Ігнорування cond. Формально коректний розв'язок погано обумовленої системи може не мати жодного сенсу.
Підсумок¶
@— матричне множення,*— поелементне. Плутанина між ними не викликає помилки, але дає неправильний результат.- Для векторів
a @ b— скалярний добуток, число. np.linalg.normрахує довжину вектора; зaxis=1— відстані для набору точок одразу..Tтранспонує матрицю і повертає перегляд; на одновимірному масиві не робить нічого.- Множення можливе, коли внутрішні розміри збігаються:
(m, k) @ (k, n) -> (m, n). np.eyeстворює одиничну матрицю,np.diag— діагональну,np.traceрахує слід.np.linalg.det— визначник; нуль означає вироджену матрицю. Порівнювати з нулем треба через поріг.np.linalg.inv— обернена матриця, кидаєLinAlgErrorна виродженій.np.linalg.solve(a, b)розв'язує систему \(Ax = b\) — точніше й швидше заinv(a) @ b.- Результат завжди перевіряють через
np.allclose(a @ x, b). np.linalg.matrix_rankіnp.linalg.condпоказують, чи має система надійний розв'язок.np.linalg.lstsqзнаходить наближений розв'язок, коли рівнянь більше, ніж невідомих — основа лінійної регресії.
Корисні посилання¶
- NumPy: linear algebra
np.linalg.solvenp.linalg.invnp.linalg.detnp.linalg.normnp.linalg.lstsqnp.linalg.matrix_rank- PEP 465 — оператор
@
Знайшли помилку чи бажаєте додати інформацію, щоб покращити курс? Створіть issue на GitHub