Вектор оптимума: Градиентный метод для поиска лучших решений
Вектор оптимума: Градиентный метод для поиска лучших решений

Полная версия

Вектор оптимума: Градиентный метод для поиска лучших решений

Настройки чтения
Размер шрифта
Высота строк
Поля
На страницу:
2 из 3

\[ \kappa=\frac{\lambda{\max}}{\lambda{\min}} \]

называют числом обусловленности. В данном примере \(\kappa=100\). У круглой чаши обе кривизны одинаковы, поэтому \(\kappa=1\): задача хорошо обусловлена. Чем сильнее вытянуты эллипсы, тем больше число обусловленности и тем обычно труднее базовому градиентному спуску быстро приблизиться к минимуму.

Для гладкой сильно выпуклой функции часто рассматривают границы кривизны: минимальную \(\mu>0\) и максимальную \(L\). Их отношение \(L/\mu\) играет роль глобального числа обусловленности. При стандартных предположениях скорость простого градиентного спуска по числу итераций ухудшается примерно пропорционально этому отношению. Это не означает, что каждую задачу с большим числом обусловленности невозможно решить. Это означает, что геометрия может заставить простой алгоритм делать много осторожных шагов.

Если минимальная кривизна равна нулю, ситуация становится особенной. Например, у \(f(x,y)=x^2\) функция совсем не меняется вдоль \(y\). В этом направлении нет ни склона, ни единственного места, куда нужно прийти. Для такого случая конечное число обусловленности в приведённом смысле не получается: минимальная кривизна отсутствует. Это не обязательно ошибка задачи, но важный сигнал — решение может быть неединственным, а движение в плоском направлении не будет уточнять ответ.

Число обусловленности зависит и от того, как заданы координаты. Если один параметр записан в метрах, а другой — в миллиметрах, геометрия на графике может выглядеть искусственно растянутой. Даже одинаковая физическая перемена в разных единицах получает разные численные размеры. Поэтому перед выводом о «важности» параметра стоит проверить масштабирование. Нормирование признаков, перевод параметров в сопоставимые единицы и методы предобуславливания способны заметно улучшить маршрут поиска. Но они не превращают автоматически невыпуклую задачу в выпуклую и не меняют смысл цели, если преобразование выполнено корректно.

Как читать карту на практике

Визуальная диагностика полезна, если рисовать её аккуратно. Сначала выберите два параметра, зафиксируйте остальные и измерьте значение функции на сетке вокруг интересующей точки. Затем постройте линии одинакового значения. Для сравнения разных карт используйте сопоставимые интервалы уровней и проверьте масштаб осей: круг, нарисованный при неодинаковом масштабе по горизонтали и вертикали, легко принять за эллипс.

Ищите три признака. Первый — вытянутость контуров: она указывает на направления с различной чувствительностью. Второй — сгущение линий: оно говорит о более резком изменении функции поперёк них. Третий — число и расположение отдельных впадин. Несколько замкнутых семейств контуров могут означать несколько областей притяжения и повышенную зависимость результата от начальной точки.

Однако двумерная картинка — только срез многомерной задачи. Пока меняются два параметра, остальные обычно удерживаются фиксированными. В другом срезе рельеф может выглядеть иначе; важное направление может быть комбинацией сразу нескольких параметров. Поэтому контурный график — не полная карта, а полезный снимок участка местности.

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

Упражнение: оцените маршрут

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


Карта

Что видно на линиях уровня

Ожидаемая трудность


А

Вокруг одного центра расположены почти круглые вложенные контуры

Относительно лёгкий поиск: направления меняются сходным образом


Б

Контуры — длинные узкие эллипсы с отношением осей около \(30:1\)

Высокая обусловленность: вероятны медленный прогресс вдоль долины и колебания поперёк


В

Видно несколько отдельных областей замкнутых контуров, а между ними — седловидные проходы

Функция, вероятно, невыпукла: результат может зависеть от начальной точки, глобальный минимум не гарантирован


Проверьте себя: карта А не обещает мгновенного решения, но её геометрия наиболее благоприятна. Карта Б прежде всего предупреждает о трудности масштаба и шага. Карта В заставляет задать другой вопрос: в какую именно впадину попадёт алгоритм и существует ли более глубокая? Контуры помогают сформулировать прогноз, но сами по себе не доказывают, что функция выпукла или что найденная точка глобально лучшая.

Рельеф целевой функции — не украшение к формулам, а часть самой задачи. Линии уровня показывают, где ошибка одинакова; выпуклость объясняет, когда локальный успех означает глобальный; обусловленность раскрывает, почему узкие долины замедляют поиск. Когда один параметр будто движется свободно, а другой требует усилий, полезно смотреть не только на их значения, но и на карту, которую они вместе вычерчивают. А чтобы научиться понимать, почему градиент выбирает именно такое направление шага, теперь пора рассмотреть его как компас, построенный из производных.

Градиент как компас: производные, направления и цепное правило

Аналитик меняет один параметр модели наугад: добавляет к нему сотую долю, запускает расчёт и ждёт. Ошибка уменьшается. Хороший знак — попробуем прибавить ещё. Теперь ошибка растёт. Он возвращает параметр назад, пробует другой шаг, затем переключается на соседний параметр. День уходит на серию опытов, каждый из которых сообщает лишь одно: «здесь стало лучше» или «здесь стало хуже». Но насколько лучше? И куда двигаться дальше?

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

Сначала — скорость изменения

Представим, что ошибка модели зависит только от одного параметра \(w\), а её значение описывается простой функцией:

\[ L(w)=(w-3)^2. \]

Если сейчас \(w=1\), ошибка равна \(4\). Аналитик увеличивает параметр на \(0{,}1\): новое значение ошибки — \(3{,}61\). Изменение параметра составило \(0{,}1\), а изменение ошибки — \(-0{,}39\). Средняя скорость изменения на этом маленьком отрезке равна

\[ \frac{3{,}61-4}{0{,}1}=-3{,}9. \]

Знак минус важен: при увеличении \(w\) ошибка уменьшается. Если сделать шаг ещё меньше, отношение изменения ошибки к изменению параметра будет приближаться к \(-4\). Это и есть производная \(L'(1)\).

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

Формально производную можно представить как предел отношений малых изменений:

\[ L'(w)=\lim{h\to 0}\frac{L(w+h)-L(w)}{h}. \]

В практической работе мы не обязаны вычислять этот предел каждый раз с нуля. Часто производную удаётся получить аналитически. Но её смысл остаётся тем же: это локальная скорость изменения. Если производная положительна, увеличение параметра вблизи данной точки повышает функцию. Если отрицательна — снижает. Если близка к нулю, функция в этом направлении меняется медленно, хотя это ещё не доказывает, что найден минимум.

От одного параметра к карте чувствительности

Обычно у модели не один параметр, а целый набор: веса, смещения, коэффициенты, настройки. Обозначим их вектором \(\boldsymbol{\theta}=(\theta1,\theta2,\ldots,\thetan)\). Целевая функция теперь зависит сразу от нескольких переменных: \(L(\boldsymbol{\theta})\).

Чтобы понять её поведение, можно начать по очереди менять каждый параметр, оставляя остальные неизменными. Так мы получим частные производные:

\[ \frac{\partial L}{\partial \theta1},\quad \frac{\partial L}{\partial \theta2},\quad \ldots,\quad \frac{\partial L}{\partial \thetan}. \]

Каждая частная производная — отдельный показатель чувствительности. Например, \(\partial L/\partial \theta2\) сообщает, как изменится ошибка, если немного изменить \(\theta2\), не трогая остальные параметры. Это похоже на то, как инженер проверяет отдельные регуляторы на пульте: один поворачивает, все остальные пока оставляет на месте.

Соберём эти числа в вектор — градиент:

\[ \nabla L(\boldsymbol{\theta})= \left( \frac{\partial L}{\partial \theta1}, \frac{\partial L}{\partial \theta2}, \ldots, \frac{\partial L}{\partial \thetan} \right). \]

Важное различие: значение функции — число, частная производная — число, градиент — вектор из частных производных. Если целевая функция показывает высоту местности, то градиент в каждой точке описывает её локальный наклон сразу по всем координатам.

Для небольшого изменения параметров \(\Delta\boldsymbol{\theta}\) изменение функции можно приблизительно оценить так:

\[ \Delta L\approx \nabla L\cdot \Delta\boldsymbol{\theta}. \]

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

Стрелка на ландшафте

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

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

Но почему именно в сторону самого быстрого роста? Возьмём любое направление длины один — то есть направление, в котором мы сравниваем одинаково короткие шаги. Скорость изменения функции в нём равна скалярному произведению градиента на этот единичный вектор. По неравенству Коши — Буняковского это произведение максимально, когда направление шага совпадает с направлением градиента. Иными словами, градиент указывает туда, где функция растёт быстрее всего. Противоположное направление даёт самое быстрое локальное убывание.

Вот почему, если мы хотим уменьшить ошибку, двигаться нужно против градиента. Минус перед градиентом не случайная договорённость и не магический рецепт. Это геометрическое следствие того, как устроена скорость изменения функции.

Рассмотрим простой ландшафт:

\[ f(x,y)=x^2+4y^2. \]

Его линии уровня — эллипсы: на каждой из них значение \(f\) одинаково. Градиент равен

\[ \nabla f(x,y)=(2x,8y). \]

В точке \((1,1)\) он составляет \((2,8)\). Стрелка градиента направлена перпендикулярно эллипсу через эту точку и показывает самый крутой подъём. Для спуска мы выбираем противоположное направление: \((-2,-8)\).

Можно сделать небольшой шаг длиной, заданной числом \(\alpha=0{,}05\), вдоль отрицательного градиента:

\[ (1,1)-0{,}05(2,8)=(0{,}9,0{,}6). \]

В исходной точке функция равна \(5\). В новой точке она равна \(0{,}9^2+4\cdot 0{,}6^2=2{,}25\). Значение уменьшилось. Градиент здесь сработал как компас: он сообщил, куда локально выгоднее направить движение.

Однако компас не выбирает за нас длину шага. Слишком длинный шаг может проскочить через подходящую область; слишком короткий будет продвигать нас медленно. Это уже вопрос алгоритма градиентного спуска — к нему мы подойдём в следующей главе.

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

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

Цепное правило: как передать чувствительность назад

На практике целевая функция редко записана одной короткой формулой. Чаще она складывается из последовательности простых вычислений: параметры создают предсказание, предсказание сравнивается с ответом, разница превращается в ошибку. Чтобы найти, как каждый исходный параметр влияет на конечный результат, нужно пройти эту цепочку связей. Здесь работает цепное правило.

Рассмотрим небольшую модель. Она получает вход \(x\), умножает его на вес \(w\), добавляет смещение \(b\), возводит результат в квадрат и сравнивает с целевым значением \(y\). Обозначим промежуточные вычисления:


Этап

Вычисление

Значение в примере


1

\(s=wx+b\)

\(2\)


2

\(a=s^2\)

\(4\)


3

\(r=a-y\)

\(3\)


4

\(L=\frac12r^2\)

\(4{,}5\)


Возьмём \(x=2\), \(w=1\), \(b=0\), \(y=1\). Прямой проход прост: сначала получаем \(s=2\), затем \(a=4\), остаток \(r=3\), а итоговая ошибка \(L=4{,}5\).

Теперь спросим не только «какова ошибка?», но и «как она зависит от \(w\) и \(b\)?». Начнём с последней операции. Производная \(\frac12r^2\) по \(r\) равна \(r\), то есть здесь равна \(3\). Это сообщает, насколько чувствительна ошибка к изменению остатка.

Остаток \(r=a-y\) меняется вместе с \(a\) с коэффициентом \(1\). Значит, чувствительность ошибки к \(a\) тоже равна \(3\). Следующая операция — \(a=s^2\). Её производная по \(s\) равна \(2s\), то есть \(4\). Умножаем локальные чувствительности: влияние \(s\) на итоговую ошибку равно \(3\cdot4=12\).

Осталось вернуться к параметрам. Поскольку \(s=wx+b\), его производная по \(w\) равна \(x=2\), а по \(b\) — \(1\). Поэтому:

\[ \frac{\partial L}{\partial w}=12\cdot2=24, \qquad \frac{\partial L}{\partial b}=12\cdot1=12. \]

Вектор градиента по параметрам \((w,b)\) равен \((24,12)\). При текущем выборе параметров увеличение \(w\) или \(b\) локально повышает ошибку. Для её уменьшения нужно двигаться в противоположную сторону.

Цепное правило здесь выглядит как последовательная передача чувствительности от результата к источникам. Для функции, которая зависит от промежуточной величины, это записывается так:

\[ \frac{dL}{dw}= \frac{dL}{dr} \cdot \frac{dr}{da} \cdot \frac{da}{ds} \cdot \frac{ds}{dw}. \]

Каждый множитель отвечает за один переход: как меняется следующий узел вычисления, если немного изменить предыдущий. Сложную производную мы не угадываем целиком — раскладываем на короткие локальные производные и перемножаем их. Именно поэтому градиенты можно эффективно считать даже для больших вычислительных цепочек: достаточно знать операции и связи между ними. Если один промежуточный результат влияет на несколько ветвей вычисления, вклады этих ветвей складываются.

Проверка: не верьте формуле вслепую

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

Для параметра \(w\) используем центральную разность:

\[ \frac{\partial L}{\partial w} \approx \frac{L(w+h)-L(w-h)}{2h}. \]

В нашем примере, если \(b=0\), то \(L(w)=\frac12(4w^2-1)^2\). Аналитически мы получили \(\partial L/\partial w=24\) при \(w=1\). Возьмём \(h=0{,}0001\). Центральная разность даст приблизительно \(24{,}00000032\) — почти то же самое. Значит, формула, по крайней мере в этой точке, выдержала проверку.

Почему нельзя выбрать любое \(h\)? Если шаг слишком велик, мы измеряем наклон на заметном участке кривой, а не локальную скорость в точке. Кривизна поверхности искажает результат. Если шаг слишком мал, значения \(L(w+h)\) и \(L(w-h)\) становятся почти одинаковыми. При вычитании теряется точность: вычислительная арифметика не хранит бесконечное число знаков, а разность близких чисел особенно чувствительна к округлению.

Практический подход — проверить производную несколькими разумными значениями \(h\). Если результаты заметно расходятся, возможно, шаги слишком большие или слишком маленькие, функция шумная либо в точке есть излом. Центральная разность обычно точнее односторонней для гладкой функции, но требует двух вычислений вместо одного. Для параметров разного масштаба полезно подбирать и разные шаги проверки.

Вот короткий маршрут для самостоятельной проверки:

1. Выберите точку и параметр, например \(w=1\), \(b=0\).

2. Найдите аналитическое значение частной производной.

3. Вычислите конечную разность с небольшим шагом \(h\).

4. Повторите проверку с шагом примерно в десять раз меньше и в десять раз больше.

5. Сравните результаты и проверьте, не объясняется ли расхождение округлением, кривизной или особенностью функции.

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

Что взять с собой

Производная измеряет локальную скорость изменения функции. Частные производные показывают чувствительность к отдельным параметрам, а градиент объединяет эти чувствительности в вектор. Геометрически он перпендикулярен линии уровня и направлен к наиболее быстрому росту; чтобы уменьшать целевую функцию, нужно двигаться против него. Если вычисления состоят из цепочки операций, цепное правило позволяет передать влияние результата назад, шаг за шагом. А конечные разности дают простой способ проверить, верны ли полученные производные.

Теперь случайные изменения параметров можно заменить осмысленным вопросом: «Как функция отреагирует на такой шаг?» Следующий шаг — превратить ответ градиента в последовательность движений, которая будет систематически приближать нас к меньшей ошибке.

Градиентный спуск: от первой итерации к работающему алгоритму

В 23:17 разработчик смотрит на график прогноза и видит знакомую неприятность: модель вроде обучилась, но промахивается снова и снова. У неё всего два настраиваемых параметра — два условных регулятора, которыми можно поправить поведение. Но как именно оптимизатор решает, куда крутить каждый из них? Почему после одной итерации ошибка уменьшается, а после другой может взлететь?

Чтобы увидеть работу градиентного спуска изнутри, не нужны большая нейросеть и часы вычислений. Достаточно функции двух переменных, карандаша и нескольких аккуратных шагов. Представим, что \(x\) и \(y\) — отклонения параметров модели от их лучших значений. Её ошибку задаёт простая функция:

\[ f(x,y)=x^2+2y^2. \]

Минимум здесь находится в точке \((0,0)\): чем ближе параметры к ней, тем меньше ошибка. При этом отклонение по \(y\) обходится дороже, чем такое же отклонение по \(x\): его вклад в ошибку вдвое больше. Это учебный пример, но механика, которую мы сейчас разберём, та же, что работает в более сложных моделях.

Одна итерация: посчитать, посмотреть, шагнуть

Начнём с параметров \((2,1)\). Сначала вычислим ошибку:

\[ f(2,1)=2^2+2\cdot1^2=6. \]

Затем найдём градиент — соберём частные производные по обеим переменным:

\[ \nabla f(x,y)= \begin{pmatrix} 2x\\ 4y \end{pmatrix}. \]

В стартовой точке он равен

\[ \nabla f(2,1)= \begin{pmatrix} 4\\ 4 \end{pmatrix}. \]

Градиент указывает направление наиболее быстрого роста функции. Значит, для уменьшения ошибки будем двигаться в противоположную сторону. Правило обновления параметров выглядит так:

\[ \theta{k+1}=\thetak-\eta\nabla f(\thetak), \]

где:

• \(\thetak\) — текущие параметры на шаге \(k\);

• \(\nabla f(\thetak)\) — градиент, вычисленный именно в текущей точке;

• \(\eta\) — скорость обучения, или размер шага;

• \(\theta{k+1}\) — новые параметры после обновления.

В нашем примере \(\theta0=(2,1)\). Возьмём \(\eta=0{,}1\). Тогда:

\[ \theta1= \begin{pmatrix} 2\\ 1 \end{pmatrix} - 0{,}1 \begin{pmatrix} 4\\ 4 \end{pmatrix} = \begin{pmatrix} 1{,}6\\ 0{,}6 \end{pmatrix}. \]

Ошибка в новой точке:

\[ f(1{,}6,0{,}6)=1{,}6^2+2\cdot0{,}6^2 =2{,}56+0{,}72=3{,}28. \]

Она снизилась с 6 до 3,28. Вот первая итерация во всей полноте: мы вычислили значение функции, нашли градиент, выбрали размер шага, обновили параметры и проверили результат.

Обратите внимание: градиент показывает, куда функция растёт быстрее всего, но шаг не обязан приводить сразу к минимуму. Он только предлагает направление и расстояние. Подобно тому как один поворот руля меняет курс машины, но ещё не доставляет её к месту назначения.

Несколько шагов на бумаге

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


Шаг \(k\)

Текущие параметры \((x,y)\)

Градиент \(\nabla f\)

Новые параметры \((x,y)\)

Ошибка в новой точке


0

\((2,\ 1)\)

\((4,\ 4)\)

\((1{,}6,\ 0{,}6)\)

\(3{,}28\)


1

\((1{,}6,\ 0{,}6)\)

\((3{,}2,\ 2{,}4)\)

\((1{,}28,\ 0{,}36)\)

\(1{,}8976\)


2

\((1{,}28,\ 0{,}36)\)

\((2{,}56,\ 1{,}44)\)

\((1{,}024,\ 0{,}216)\)

\(1{,}141888\)


3

\((1{,}024,\ 0{,}216)\)

\((2{,}048,\ 0{,}864)\)

\((0{,}8192,\ 0{,}1296)\)

\(0{,}704517\)


Проверим второй шаг. В точке \((1{,}6,0{,}6)\) градиент равен \((3{,}2,2{,}4)\). Поэтому:

\[ (1{,}6,0{,}6)-0{,}1(3{,}2,2{,}4)=(1{,}28,0{,}36). \]

Ошибка там равна \(1{,}28^2+2\cdot0{,}36^2=1{,}8976\). Всё повторяется по одному и тому же правилу, а параметры постепенно приближаются к нулю.

Эта таблица полезна не только как иллюстрация. Она позволяет отлаживать алгоритм без доверия к «чёрному ящику»: если программа на тех же исходных условиях выдаёт совершенно другие координаты, можно проверить вычисления по шагам. Например, первая новая точка обязана быть \((1{,}6,0{,}6)\). Если это не так, ошибка возникла раньше, чем алгоритм успел стать сложным.

Ловушка, в которую легко попасть

В какой-то момент разработчик замечает: производные вычисляются правильно, но ошибка после обновления почему-то растёт. Причина оказывается в одной мелочи — знаке.

Если вместо вычитания прибавить градиент, получится:

\[ \theta1= \begin{pmatrix} 2\\ 1 \end{pmatrix} + 0{,}1 \begin{pmatrix} 4\\ 4 \end{pmatrix} = \begin{pmatrix} 2{,}4\\ 1{,}4 \end{pmatrix}. \]

Тогда ошибка станет

\[ f(2{,}4,1{,}4)=2{,}4^2+2\cdot1{,}4^2=9{,}68, \]

то есть вырастет с 6 до 9,68. Формула градиента записана верно, вычисление градиента верно, размер шага тот же — но программа пошла вверх вместо того, чтобы спускаться.

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

Есть и другая тонкость: все компоненты градиента нужно считать в одной и той же текущей точке. Сначала вычислите полный градиент, затем одновременно обновите параметры. Если изменить \(x\), а потом пересчитать производную по \(y\) уже с новым \(x\), получится другой алгоритм — последовательное покоординатное обновление. Оно тоже может быть полезным, но это уже не та итерация, которую мы записали в формуле.

На страницу:
2 из 3