Инструменты

Градиентный спуск

Что такое оптимизация и зачем она нужна?

Представьте, что вы решили испечь идеальное печенье. Перед вами ‒ привычный набор ингредиентов: мука, масло, сахар, шоколад, ваниль. Меняя их пропорции — добавляя больше шоколада, меньше муки, отказываясь от ванили или, наоборот, усиливая её аромат — вы каждый раз получаете новое печенье. С каждой пробой вы стремитесь понять, какой именно рецепт даст наилучший вкус и текстуру.

По сути, это и есть оптимизация — поиск таких параметров, которые приводят к наилучшему результату. Она встречается повсюду. Мы выбираем кратчайший маршрут до работы, настраиваем температуру воды в душе, чтобы она была «в самый раз», подбираем состав сплава для прочных деталей и, конечно, балансируем себестоимость и прибыль на производстве. 

В более широком смысле, оптимизация — это процесс поиска наилучшего или наиболее эффективного решения для определённой задачи из набора возможных вариантов. Цель обычно заключается в том, чтобы минимизировать что-то нежелательное (например, затраты, ошибки, время) или максимизировать что-то желаемое (например, прибыль, точность, производительность).

Мы уже поняли, что оптимизация — это поиск идеального набора ингредиентов. Но вот задача: возможных сочетаний муки, сахара, масла и шоколада — миллионы, а пробовать каждое вручную невозможно. Нужно что-то умнее, чем «печь-пробовать-угадывать».

Представьте, что после каждой партии вы не просто дегустируете печенье, а ставите ему числовую оценку вкуса — чем выше балл, тем лучше. Теперь можно подходить к рецепту как к науке: изменили грамм шоколада — посмотрели, как изменился балл; убавили сахар — снова измерили. Если небольшое увеличение шоколада подняло оценку на 0,3 пункта, а уменьшение сахара снизило на 0,1 пункта, очевидно, шоколада стоит добавить, а сахара убавить. Так, опираясь на реальные «оценки вкуса», вы делаете маленькие, но направленные шаги в сторону идеального рецепта вместо хаотичных скачков.

Именно такой методический подход называют градиентным спуском. В машинном обучении «оценкой вкуса» служит функция ошибки (или функцию потерь): представьте, что идеальный рецепт печенья даёт потери 0, а любые отклонения (слишком сухое, мало сахара) увеличивают их. Чем ниже эти «потери» (ошибка), тем точнее и «вкуснее» (идеальнее) наша модель.

Градиент  показывает, как меняется эта ошибка, если немного подвинуть каждый параметр; а спуск означает последовательное уменьшение ошибки малыми поправками. Так шаг за шагом, он «пробует» новые комбинации параметров, оценивает результат и корректирует курс, словно пекарь, который после каждой партии печенья корректирует «рецепт» модели, пока та не начнёт давать наилучший результат.

Что такое градиент?

Представьте, что наш градиент — это такой «компас», который помогает нам найти идеальный рецепт печенья. Помните, мы шаг за шагом «оценивали вкус» после того, как меняли количество каждого ингредиента? Так вот, градиент делает нечто похожее, только сразу для всех ингредиентов! Он показывает, в каком направлении нужно «подвинуть» каждый из них (чуть добавить муки, немного убавить сахар), чтобы «вкус» нашего печенья стал максимально приближен к идеалу. По сути, это вектор, который собирает в себе все эти «подсказки» по изменению каждого ингредиента для достижения лучшего результата.


Мы уже отлично поняли интуицию, стоящую за оптимизацией и градиентным спуском, используя наше «идеальное печенье». Теперь пришло время перевести эти идеи на язык математики, чтобы вы увидели, как «компас» градиента выглядит в числах и символах.

Математика градиентного спуска

Градиент функции [math]f[/math] — это вектор, состоящий из производных ответа по каждому аргументу.

Градиент на письме часто обозначают символом [math]\nabla[/math], который называется оператор наблаГрадиент функции [math]f[/math] от [math]n[/math]-мерного вектора [math]x[/math] вычисляется так: $$\nabla f(\mathbf{x}) = \left( \frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \dots, \frac{\partial f}{\partial x_n} \right)$$Это математическое выражение — сердцевина всего процесса. Каждая частичная производная [math] \frac{\partial f}{\partial x_i}[/math] показывает, насколько функция [math] f[/math] изменится, если мы чуть-чуть подвинем переменную [math] x_i[/math] , оставив все остальные на месте. 

Что в контексте градиентного спуска представляет собой градиент функции потерь?

Вернемся к нашему любимому печенью. Представьте, что функция [math]f[/math] — это наша "оценка вкуса" (или, в терминах машинного обучения, функция потерь), а вектор [math]\mathbf{x}[/math] — это набор ингредиентов: [math] x_1[/math] — количество муки, [math]x_2[/math] — сахара, [math]x_3[/math] — шоколада и так далее. Градиент [math]\nabla f(\mathbf{x})[/math] — это как раз тот "компас", который говорит: "Если добавить чуть-чуть муки ([math]\partial f / \partial x_1[/math]), вкус ухудшится на 0.2 пункта? Тогда лучше не добавлять! А если уменьшить сахар ([math]\partial f / \partial x_2[/math]), оценка вырастет на 0.5? Отлично, делаем это!"

Математически каждая компонента градиента — это частная производная. Она показывает, насколько изменится значение функции [math]f[/math], если мы слегка подвинем только один параметр, оставив остальные на месте. Если производная положительная, значит, увеличение этого параметра повышает "потери" (делает печенье хуже), и нам нужно двигаться в противоположном направлении. Если отрицательная — наоборот.

Теперь давайте разберемся, как этот градиент помогает в спуске. 

Градиентный спуск — это итеративный алгоритм, который шаг за шагом корректирует параметры, чтобы минимизировать функцию [math]f[/math]. Формула обновления выглядит просто, но мощно: $$\mathbf{x}^{(k+1)} = \mathbf{x}^{(k)} - \eta \cdot \nabla f(\mathbf{x}^{(k)})$$ Здесь [math]\mathbf{x}^{(k)}[/math] — текущие параметры на шаге [math]k[/math] (текущий рецепт), [math]\eta[/math] (эта) — скорость обучения (как большой шаг мы делаем за раз), а минус перед градиентом означает, что мы двигаемся в сторону уменьшения функции (вниз по "холму" ошибок, к идеальному печенью).

Почему минус? Потому что градиент указывает направление наибольшего роста функции — как стрелка, показывающая "вверх по склону". Чтобы спускаться к минимуму, мы поворачиваем в обратную сторону. Это как если бы вы стояли на холме с завязанными глазами: градиент — это ощущение уклона под ногами, и вы шагаете вниз, против уклона.

Почему в алгоритме градиентного спуска обновление параметров происходит в сторону антиградиента, а не градиента?

Скорость обучения [math]\eta[/math] — ключевой ингредиент. Если она слишком маленькая (скажем, [math]0.001[/math]), вы будете ползти черепашьими шажками: безопасно, но медленно, и может уйти вечность на достижение минимума. Если слишком большая (например, [math]1.0[/math]), вы рискуете перепрыгнуть через "долину" идеального рецепта и улететь в противоположную сторону, где печенье станет еще хуже. В худшем случае алгоритм может даже разойтись — "взрываться" в бесконечность. Поэтому [math]\eta[/math] подбирают экспериментально, иногда даже адаптируя ее на ходу (как в методах Adam или RMSprop, но об этом позже).

Какова основная цель алгоритма градиентного спуска в машинном обучении?

Пример: Градиентный спуск в линейной регрессии с MSE

Чтобы все стало наглядно, давайте возьмем полноценный пример из машинного обучения — линейную регрессию. Это как если бы мы пытались предсказать "вкус" печенья [math]y[/math] на основе количества шоколада [math]x[/math], подбирая параметры модели$$y = w \times x + b$$ где [math]w[/math] — коэффициент (наклон линии), [math]b[/math] — смещение (сдвиг по оси [math]y[/math]).

Мы используем MSE (Mean Squared Error) как функцию потерь — одну из самых популярных для регрессии. Она измеряет среднеквадратичную ошибку между реальными значениями и предсказаниями: $$\text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y_i})^2$$

где [math]\hat{y_i} = w \cdot x_i + b[/math], [math]n[/math] — количество примеров.

Градиенты для MSE вычисляются так: $$\frac{\partial \text{MSE}}{\partial w} = -\frac{2}{n} \sum_{i=1}^{n} (y_i - \hat{y_i}) \cdot x_i$$$$\frac{\partial \text{MSE}}{\partial b} = -\frac{2}{n} \sum_{i=1}^{n} (y_i - \hat{y_i})$$

Обновления параметров: $$ w^{(k+1)} = w^{(k)} - \eta \cdot \frac{\partial \text{MSE}}{\partial w}$$$$b^{(k+1)} = b^{(k)} - \eta \cdot \frac{\partial \text{MSE}}{\partial b}$$

Какое важное свойство должна иметь функция потерь для успешного применения градиентного спуска?

Данные для примера

Возьмем простой набор данных: три точки, которые идеально лежат на линии [math]y = 2x[/math]
(без шума, чтобы было легко видеть прогресс). 

  • [math]x = [1, 2, 3][/math]
  • [math]y = [2, 4, 6][/math] 

Идеальные параметры: [math]w = 2[/math], [math]b = 0[/math]. Но мы начнем с "плохого" рецепта: [math]w = 0[/math], [math]b = 0[/math]. Скорость обучения [math]\eta = 0.1[/math] (не слишком большая, чтобы избежать колебаний).

Инициализация

Итерация 0: Начальная точка
  • Текущие параметры: $w = 0.00$, $b = 0.00$
  • Предсказания модели:
    • для $x = 1 \to 0$
    • для $x = 2 \to 0$
    • для $x = 3 \to 0$
  • MSE (Среднеквадратическая ошибка): $18.67$ — ошибка очень высокая.
  • Расчет градиентов (сигналы):
    • Градиент по $w$: $-18.67$ (сигнал увеличить $w$)
    • Градиент по $b$: $-8.00$ (сигнал увеличить $b$)
Обновление параметров ($\eta=0.1$):
$$w = 0 - 0.1 \cdot (-18.67) = \mathbf{1.87}$$ $$b = 0 - 0.1 \cdot (-8.00) = \mathbf{0.80}$$

Итерации градиентного спуска

Алгоритм начинает "спускаться" по склону ошибки к минимуму.

Итерация 1
  • Параметры: $w = 1.87$, $b = 0.80$
  • MSE: $0.30$ (уже значительно лучше!)
  • Градиенты: по $w$: $1.96$, по $b$: $1.07$
Обновление:
$w = 1.87 - 0.1 \cdot 1.96 = \mathbf{1.67}$
$b = 0.80 - 0.1 \cdot 1.07 = \mathbf{0.69}$
Итерация 2
  • Параметры: $w = 1.67$, $b = 0.69$
  • MSE: $0.07$
  • Градиенты: по $w$: $-0.30$, по $b$: $0.07$
Обновление:
$w = 1.67 - 0.1 \cdot (-0.30) = \mathbf{1.70}$
$b = 0.69 - 0.1 \cdot 0.07 = \mathbf{0.69}$
Итерация 3
  • Параметры: $w = 1.70$, $b = 0.69$
  • MSE: $0.07$
  • Градиенты: по $w$: $-0.05$, по $b$: $0.18$
Обновление:
$w = 1.70 - 0.1 \cdot (-0.05) = \mathbf{1.71}$
$b = 0.69 - 0.1 \cdot 0.18 = \mathbf{0.67}$
Итерация 4
  • Параметры: $w = 1.71$, $b = 0.67$
  • MSE: $0.06$
  • Градиенты: по $w$: $-0.07$, по $b$: $0.16$
Обновление:
$w = 1.71 - 0.1 \cdot (-0.07) = \mathbf{1.71}$
$b = 0.67 - 0.1 \cdot 0.16 = \mathbf{0.65}$

Финальный результат после 5 итераций
  • Оптимизированные параметры: $w \approx 1.71$, $b \approx 0.65$
  • Итоговая MSE: $\approx 0.06$ (очень близко к идеалу).

Примечание: С шагом обучения $\eta=0.1$ спуск происходит медленно. За большее число итераций параметры приблизились бы к идеальным значениям ($w=2, b=0$).

Видите? Шаг за шагом градиент «корректирует рецепт»: сначала большие изменения (когда ошибка высока), потом мелкие уточнения. Если функция не выпуклая, с несколькими «ямами» (локальными минимумами), градиентный спуск может застрять в неидеальной «яме» — как если бы вы нашли рецепт «хорошего» печенья, но пропустили «идеальное» в другой долине. 

Визуальное выделение минимумов помогает понять проблему оптимизации в машинном обучении: алгоритмы могут "застревать" в локальных минимумах вместо достижения глобального.
Итерация: 0 Позиция b = 6.600 Градиент k =

Для таких случаев есть улучшения: стохастический градиентный спуск (SGD), где мы используем случайные подвыборки данных для «шумов», помогающих выскочить из ловушек, или momentum — как добавление инерции, чтобы не застревать.

Что, скорее всего, произойдет, если выбрать слишком большой шаг обучения (learning rate) в алгоритме градиентного спуска?

В чем заключается проблема локальных минимумов при использовании градиентного спуска?

В машинном обучении все это применяется к функциям потерь, которые мы перечислили тут. Для регрессии часто берут MSE, как в нашем примере, где градиент помогает корректировать веса модели, чтобы предсказания [math]\hat{y}[/math] были ближе к реальным [math]y[/math]. Для классификации — Log Loss или Cross-Entropy, где градиент «штрафует» за уверенность в неправильных ответах. Но помните: градиентный спуск — не панацея. Он требует, чтобы функция была дифференцируемой (чтобы производные существовали), и в реальности мы часто добавляем регуляризацию (чтобы избежать переобучения, как если бы вы не добавляли слишком много шоколада, чтобы печенье не стало приторным). 

Это и есть математика в действии — от интуиции печенья к точным формулам, которые обучают нейронные сети и оптимизируют мир вокруг нас.

Виды градиентного спуска

Мы уже разобрали базовый градиентный спуск (он же Batch Gradient Descent) — это как если бы вы каждый раз пекли огромную партию печенья из всех возможных ингредиентов, пробовали её целиком, ставили общую оценку вкуса и только потом корректировали рецепт. Такой подход точен, но медленный и ресурсоёмкий: представьте, что у вас миллионы ингредиентов (данных), и каждый раз нужно испечь всё сразу, чтобы вычислить градиент. В больших задачах машинного обучения это может занять часы или дни. Чтобы ускорить процесс и сделать его более гибким, придумали вариации градиентного спуска. Давайте разберём основные виды, продолжая нашу аналогию с печеньем: мы всё так же ищем идеальный рецепт, но теперь печём умнее.

1. Стохастический градиентный спуск (Stochastic Gradient Descent, SGD)

Вместо того чтобы печь всю партию печенья сразу (вычислять градиент по всем данным), SGD берёт только одно случайное печение — один пример данных — и корректирует рецепт на основе него. Это как если бы вы испекли одно-единственное печенье с случайным набором ингредиентов, попробовали его и сразу подкорректировали пропорции для следующего. Шаги получаются быстрыми и «‎шумными»‎ — иногда вы «пересолите»‎ сахаром, иногда недоложите шоколад, — но в среднем вы всё равно движетесь к минимуму.

Интуиция: Такой «шум» полезен! Он помогает «выпрыгнуть» из локальных минимумов — тех «хороших, но не идеальных» рецептов, где обычный спуск мог бы застрять. SGD особенно хорош для больших датасетов, где полный градиент слишком дорог в вычислениях.

Математика: Вместо суммирования по всем n примерам, градиент вычисляется только по одному случайному [math]i[/math]-му примеру: $$\nabla f(x) \approx \nabla f_i(x) = \left( \frac{\partial f_i}{\partial x_1}, \frac{\partial f_i}{\partial x_2}, \dots, \frac{\partial f_i}{\partial x_n} \right)$$Обновление: [math]x^{(k+1)} = x^{(k)} - \eta \cdot \nabla f_i(x^{(k)})[/math]


Плюсы: Быстрый, экономит память, может лучше обобщать (избегать переобучения).

Минусы: Траектория спуска «зигзагообразная», может колебаться вокруг минимума, не сходиться так гладко.


В нашем примере с линейной регрессией: Вместо расчёта MSE по всем трём точкам [math](x=[1,2,3], y=[2,4,6])[/math], SGD на каждой итерации берёт случайную пару (например, [math] x=2, y=4[/math]), вычисляет градиент только по ней и обновляет [math]w[/math] и [math]b[/math]. После многих таких "случайных проб" параметры всё равно приблизятся к [math]w=2, b=0[/math].

Какое из утверждений верно описывает стохастический градиентный спуск (SGD)?

2. Мини-батч градиентный спуск (Mini-Batch Gradient Descent)

Это компромисс между полным батчем и стохастическим: вы печёте не всю партию и не одно печенье, а небольшую группу — скажем, 32 или 128 штук. Градиент усредняется по этому мини-батчу, что делает шаги стабильнее, чем в SGD, но быстрее, чем в полном спуске.

Интуиция: Представьте, что вы пробуете не одно печенье, а небольшую тарелку — оценка вкуса будет точнее, чем от одного, но не потребует печь тонну. Это стандарт в глубоком обучении (например, в нейросетях), где мини-батчи позволяют использовать GPU эффективно.

Математика: Градиент по подмножеству [math]m[/math] примеров [math](m \ll n)[/math]:$$\nabla f(x) \approx \frac{1}{m} \sum_{i \in \text{batch}} \nabla f_i(x)$$Обновление то же: [math]x^{(k+1)} = x^{(k)} - \eta \cdot \nabla f(x^{(k)}) [/math]


Плюсы: Баланс скорости и стабильности, параллелизация на hardware.

Минусы: Нужно подбирать размер батча (слишком маленький — шумно, слишком большой — медленно).


В практике: В нашем примере с тремя точками мини-батч размером 2 мог бы взять, скажем, [1,2] и [2,4], вычислить средний градиент и обновить.

3. Градиентный спуск с импульсом (Momentum)

Обычный спуск может «‎застревать»‎ в узких «‎долинах»‎ или колебаться, как шарик, катящийся по неровной поверхности. Momentum добавляет «‎инерцию»‎: как если бы ваш пекарь не просто шёл шаг за шагом, а разгонялся на велосипеде — предыдущие шаги дают импульс, помогая преодолеть плоские участки или мелкие ямки. Это добавляет «память» о предыдущих направлениях движения. Если градиент в каком-то направлении был большим несколько шагов подряд, то следующее обновление получит дополнительный «толчок» в ту же сторону.

Что даёт:

  • Ускоряет движение в стабильных направлениях (когда градиенты долго не меняют знак).
  • Сглаживает колебания в узких или шумных областях функции потерь.
  • Помогает быстрее проходить плоские участки и области с маленькими градиентами.

Очень часто используется вместе с мини-батч SGD.

Интуиция: Представьте, что при корректировке рецепта вы учитываете не только текущую "оценку вкуса", но и то, как вы корректировали в прошлый раз. Если вы несколько раз подряд добавляли шоколад (и это работало), то продолжите добавлять по накатанной, даже если текущий шаг мал.

Математика: Вводится переменная [math]v[/math] (velocity, скорость):$$v^{(k+1)} = \gamma \cdot v^{(k)} + \eta \cdot \nabla f(x^{(k)})$$(где [math]\gamma[/math] — коэффициент momentum, обычно 0.9) [math]x^{(k+1)} = x^{(k)} - v^{(k+1)}[/math]

Здесь минус перед [math]v[/math], потому что мы спускаемся против градиента. Без momentum [math]\gamma=0[/math] это обычный спуск.


Плюсы: Ускоряет сходимость, сглаживает колебания, помогает в «‎узких»‎ функциях потерь.

Минусы: Может перелететь минимум, если [math]\gamma[/math] слишком большой.


В примере: Если в линейной регрессии градиенты по [math]w[/math] сначала большие отрицательные, momentum накопит «‎скорость»‎ в сторону увеличения [math]w[/math], быстрее доведя до 2.

4. Nesterov Accelerated Gradient (NAG)

Это улучшение momentum: вместо расчёта градиента в текущей точке, NAG «‎заглядывает вперёд»‎ — вычисляет градиент в точке, куда приведёт текущий импульс. Как если бы ваш велосипедист сначала предугадал уклон впереди и скорректировал курс заранее.

Интуиция: Вместо «слепого» разгона, вы делаете более точные повороты, избегая перелётов.

Математика: $$v^{(k+1)} = \gamma \cdot v^{(k)} + \eta \cdot \nabla f\big(x^{(k)} - \gamma \cdot v^{(k)}\big)$$(градиент в «предполагаемой» точке) [math]x^{(k+1)} = x^{(k)} - v^{(k+1)}[/math]


Плюсы: Ещё быстрее сходится, чем обычный momentum.

Минусы: Чуть сложнее реализовать.


5. Адаптивные методы: Adagrad, RMSprop, Adam

Эти методы адаптируют скорость обучения [math]\eta[/math] для каждого параметра отдельно — как если бы для муки [math]\eta[/math] была маленькой (чтобы не пересушить), а для шоколада большой (чтобы быстро добавить вкус).

  • Adagrad: Уменьшает [math]\eta[/math] для часто обновляемых параметров (накопленный квадрат градиентов в знаменателе). Хорош для разреженных данных, но [math]\eta[/math] может стать слишком маленькой.
    Формула: $$\eta_i = \frac{\eta}{\sqrt{\sum_{j=1}^n (\nabla f_i)_j^2 + \varepsilon}}$$
  • RMSprop: Улучшение Adagrad — использует экспоненциальное среднее квадратов градиентов, чтобы [math]\eta[/math] не «затухала» навсегда.$$v^{(k+1)} = \gamma \cdot v^{(k)} + (1 - \gamma) \cdot (\nabla f)^2$$$$x^{(k+1)} = x^{(k)} - \frac{\eta}{\sqrt{v^{(k+1)} + \varepsilon}} \cdot \nabla f$$
  • Adam (Adaptive Moment Estimation): Комбинирует momentum (среднее градиентов) и RMSprop (среднее квадратов). Самый популярный сейчас — как «умный пекарь» с инерцией и адаптацией.$$m^{(k+1)} = \beta_1 m^{(k)} + (1 - \beta_1) \nabla f \tag{momentum}$$$$v^{(k+1)} = \beta_2 \, v^{(k)} + (1 - \beta_2) \, (\nabla f(x^{(k)}))^2 \tag{RMS}$$$$x^{(k+1)} = x^{(k)} - \eta \cdot \frac{\hat{m}}{\sqrt{\hat{v}} + \varepsilon} \tag{с bias correction}$$

Плюсы: Автоматическая настройка, быстрое обучение, работает на сложных задачах.

Минусы: Больше гиперпараметров (β1=0.9, β2=0.999, ε=1e-8).


В нашем печенье: Adam быстро подкорректирует сахар (если градиенты по нему большие), но аккуратно тронет муку, если изменения редкие.


Выбор вида зависит от задачи: для простых — базовый спуск, для больших данных — SGD или мини-батч, для сложных ландшафтов — Adam. В библиотеках вроде PyTorch или TensorFlow эти оптимизаторы встроены, и эксперименты помогают найти лучший «рецепт» для вашей модели.

В общей формуле обновления весов [math]w_{\text{new}} = w_{\text{old}} - \alpha \nabla f(w_{\text{old}})[/math], что означает символ [math]\alpha[/math]?

Реализация градиентного спуска в Python

Мы уже разобрали математику градиентного спуска и его виды, используя нашу любимую аналогию с печеньем: от "компаса" градиента до разных стратегий выпечки — полной партии, одной штучки или мини-батча. Теперь пришло время перейти от теории к практике. В целом нам хватит базового Python с NumPy для массивов и вычислений.

Давайте напишем функцию, которая реализует классический градиентный спуск для этого случая. Код будет простым: цикл итераций, вычисление градиента, обновление параметра. Мы также сохраним историю значений [math]b[/math] и MSE, чтобы потом нарисовать график спуска.

Код PYTHON
Соединение...
</>
Результат

Зачем в машинном обучении часто прибегают к численным методам, таким как градиентный спуск, вместо решения уравнения [math]\nabla f(w) = 0[/math] аналитически?

Если градиент функции потерь в текущей точке равен нулю, что это означает для базового алгоритма градиентного спуска?

Заключение

От печенья к моделям: оптимизация — поиск идеала через градиентный спуск. Мы разобрали математику, виды (batch, SGD, mini-batch) и Python-реализацию.

Градиент учит: малые шаги в мире данных ведут к большим результатам — от нейросетей до жизни. Экспериментируйте с кодом и функциями потерь. Оптимизируйте — и преуспейте!


notebook
Подключено
Комментарии
Пред.
Право на скуку: как тишина спасает внимание и идеи

Право на скуку: как тишина спасает внимание и идеи

Содержание Показать Скука как биологический сигнал: что делает мозг, когда

помощник
Сохранить в заметки
Спросить у нейросети
Градиентный спуск
Комментарий: