Как сделать обучающую выборку

Добавил пользователь Алексей Ф.
Обновлено: 10.09.2024

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

Как это всё состыковать, и как понять какие пропорции внутри? Я хочу рандом, без всяких наложений.

Далее у меня идёт такая конструкция в коде:

x_train_split, x_val_split, y_train_split, y_val_split = train_test_split(x_train, y_train_cat, test_size=0.2)

model.fit(x_train_split, y_train_split, batch_size=32, epochs=5, validation_data=(x_val_split, y_val_split))

Здесь я разбиваю вручную. Но в голове не проясняется. Как тут вообще понять в каких пропорциях разбивается общая выборка на тестовую и далее на обучающую с валидационной?

Я пролазил туториалки по тензорфлоу, по керасу, и по аугментациям зачем-то. У меня получилось в однмо варианте создать класс, в другом словарь. Но я не понимаю где и в каком месте далее это всё регулируется и при каких обстоятельствах считывается для сравнения результатов на всех этапах.. Ютуб уже закончился. Я даже класс могу в название файла записать, или разбить вручную по папкам. Взять чужой код с каггла и сделать рабочим, но это не даёт понимания.

Upd: Так, стоп. Я же могу применить train_test_split и в первом случае? а как будет выглядеть взаимодействие и код.. со словарём из названия файлов и класса? оно же не преобразуется в вектор нампи и класс? или да? о боже мой .. %(

Деление выборки на обучающую и тестовую

Для генерации на основе рабочей выборки обучающего и тестового наборов в пакете caret используется функция createDataPartition , которая возвращает индексы обучающей выборки. Функция подходит как для классификационных задач так и для регрессионных. При этом разделение на обучающий и тестовый наборы осуществляется сбалансировано с точки зрения значения свойства.

Загрузим значения растворимости для первой выборки.

Выделим 40% в обучающую, а остальное в тестовую выборку.

Результатом будет список будет список, каждый элемент списка это вектор индексов соединений обучающей выборки.

В нашем случае в списке только один элемент - одна обучающая выборка. Если необходимо создать несколько пар обучающих/тестовых выборок, меняем значение times на нужное число.

Поскольку у нас только одна тестовая выборка, то для упрощения дальнейшего кода преобразуем train.ids из списка в вектор индексов.

Загрузим дескрипторы для первой выборки и разделим всю выборку на обучающую и тестовую

Способ разделения рабочей выборки на обучающую и тестовую на основе подобия соединений

(Peter Willett. Journal of Computational Biology. October 1999, 6(3-4): 447-457. doi:10.1089/106652799318382)

Основная идея - создание максимально представительной тестовой выборки. Это достигается выбором в тестовую выборку соединений максимально отличающихся друг от друга. Процедура итеративна, на каждом шаге к тестовому набору добавляется одно соединение, которое максимально отличается от соединений, которые уже включены в тестовую выборку.

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

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

Вызовем функцию maxDissim (требует установленного пакета proxy ), в которой передадим тестовый и обучающий наборы соединений и также укажем количество соединений, которое мы хотим переместить из обучающего набора в тестовый, например 20% (\( ^1/_5 \) от 800 = 160, с учетом того что 5 соединений мы уже выбрали следует указать 155). Чтобы ускорить выполнение функции укажем параметр randomFrac , который определяет какая доля обучающей выборки будет рассматриваться на каждом шаге поиска подобных соединений.

Перенесем эти соединения из обучающей выборки в тестовую

Кроме евклидового расстояния в качестве меры подобия можно использовать другие метрики реализованные в пакете proxy , например манхэттенское расстояние (полный список можно посмотреть выполнив summary(proxy::pr_DB) )

Отбор переменных

  1. Исключение переменных с малой вариабельностью.
  2. Исключение взаимнокоррелирующих переменных.
  3. Исключение переменных, связанных линейными зависимостями.

Исключение постоянных переменных и переменных с малой вариабельностью

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

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

freqCut - отношение количества наиболее часто встречающегося значения переменной к количеству второго по частоте встречаемости значения переменной \( \frac \)
uniqueCut - отношение числа уникальных значений переменной к общему числу объектов в выборке в процентах \( \frac>> \times 100 \)

Если значение freqCut больше заданной границы И значение uniqueCut меньше, то переменная будет отмечена как имеющая малый разброс значений. Поэтому, чтобы отметить только переменные с постоянными значениями, достаточно установить uniqueCut = 0 .

Т.е. в нашем случае для соединений обучающей выборки есть 509 дескрипторов с постоянными значениями, которые нужно удалить.

Исключение взаимнокоррелирующих переменных

Иногда для получения надежной модели необходимо удалить переменные с высокой взаимной корреляцией. Для чего можно воспользоваться функцией findCorrelation из пакета caret .

highCor содержит индексы переменных с высокой взаимной корреляцией, которые можно исключить из рассмотрения при построении модели.

Исключение переменных, связанных линейными зависимостями

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

linCombo это список из двух элементов:
linCombo$linearCombos - список найденных линейных комбинаций.
linCombo$remove - вектор индексов переменных, которые можно выразить через линейную комбинацию остальных переменных.

Т.е. всего было найдено

переменных, которые можно исключить из построения модели, воспользовавшись свойством числовых индексов

Шкалирование переменных

Шкалирование (или масштабирование) переменных обучающей выборки часто используется перед построением модели, т.к. если абсолютные значения переменных сильно отличаются между собой, то переменные с большими абсолютными значениями искусственно получат больший вес и значимость при построении некоторых моделей.

Наиболее распространенным преобразованием является центрирование и нормирование на величину стандартного отклонения
\[ x_=\frac> \]

С этой целью можно использовать функцию scale из базового пакета, или функцию preProcess из пакета caret . Удобнее использовать второй вариант, т.к. объект создаваемый функцией preProcess содержит информацию о параметрах шкалирования (например, величину среднего значения и величину стандартного отклонения для каждой переменной) и его можно легко применять к другим наборам данных, для которых необходимо спрогнозировать значения свойства по построенным моделям. Зададим способ нормировки переменных

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

Масштабируем переменные обучающей выборки и сохраним их в файл

Опция method функции preProcesss может принимать и другие значения, которые позволяют преобразовывать данные. В частности значение method = "range" приводит значения переменных к диапазону [0,1].

Построение моделей с caret

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

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

Уровень 0. Построение модели и прогноз значений для тестовой выборки.

Сперва указываются параметры процедуры обучения модели. В данном примере это 5-кратная кросс-валидация, повторенная дважды.

Опция savePredictions позволяет сохранить значения, спрогнозированные в ходе кросс-валидации.

Теперь с использованием функции train строится модель, где указываются наборы значений X и Y для обучающей выборки, указывается метод построения модели и передается параметр trControl .

В результате кросс-валидации показано как меняются статистические характеристики модели с изменением числа компонент в PLS уравнении. Оптимальным числом компонент в данном случае является 3. Именно для этого числа компонент и построена итоговая модель, которая хранится в объекте m1.pls и которую можно в дальнейшем использовать для прогноза новых выборок.

Результат можно вывести в виде графика, показывающего зависимости ошибки модели от настроечных параметров модели

Чтобы спрогнозировать значения растворимости для тестовой выборки дескрипторы тестового набора необходимо масштабировать с использованием параметров, полученных для обучающей выборки. Предварительно нужно оставить только те дескрипторы, которые присутствовали в обучающей выборке, и расположить их в той же последовательности. Для этого можно воспользоваться тем, что объект класса preProcess хранит имена дескрипторов.

В общем случае необходимо делать проверку на NA , т.к. какие-то дескрипторы возможно отсутствуют в тестовом наборе.

Сам прогноз выполняется функцией predict

Определим среднеквадратичное отклонение значения прогноза для внешней тестовой выборки

Оценки возвращаемые функцией train

Под коэффициентом детерминации, возвращаемом функцией train подразумевается обычный квадрат коэффициента корреляции Пирсона, а не рассчитываемый по формуле
\[ R^2 = 1 - \frac = 1 - \frac^(\hat_i-y_i)^2>^(y_i-\overline_)^2> \]
Если необходимо вычислить коэффициент корреляции по этой формуле, это надо делать вручную, т.е. написать функцию. Значения, спрогнозированные в ходе кросс-валидации хранятся в объекте m1.pls$pred .

Задание для самостоятельного выполнения

Написать функцию, которая бы принимала в качестве единственного параметра объект класса train ( m1.pls ), и возвращала для выбранной модели значение \( R^2 \), рассчитанное по формуле приведенной выше.

Уровень 1. Управление настроечными параметрами модели и параметрами кросс-валидации

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

Фолды это списки с индексами соединений обучающей выборки для каждого фолда

Созданные один раз фолды можно сохранить.

Для pls метода возможно оптимизация только числа компонент в уравнении ncomp

Теперь задаем параметры обучения модели через функцию trainControl , куда передаем индексы фолдов, а в качестве метода указываем LGOCV . Последнее не принципиально, т.к. при передаче параметра index , параметр method уже не учитывается. Но из соображений удобства лучше использовать LGOCV .

Уровень 2. Использование многопоточности при построении моделей.

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

Параллельное выполнение кода приводит к дублированию в памяти используемых данных, поэтому при запуске построения модели в многопоточном режиме надо учитывать этот факт. Использование пакета doMC возможно только в Unix-системах и дает возможность избежать многократного дублирования данных в памяти.

Теперь процесс построения модели по умолчанию будет использовать несколько потоков. Сравним два подхода.

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

Запущенный кластер можно остановить в любой момент, используя функцию stopImplicitCluster (только в случае использования пакета doParallel ).

Уровень 3. Оценка важности переменных.

В пакете caret предусмотрена функция varImp , которая возвращает значения важности переменных. Эта функция содержит несколько параметров.

object - объект класса train , который возвращает одноименная функция из пакета caret useModel - следует ли использовать оценку важности переменных используя построенную модель. Возможно для ряда моделей, которые имеют собственный способ оценки важности переменных (randomForest, pls, lm, gbm и т.д., полный список доступен в справке по описываемой функции). nonpara - если для модели нет собственноо алгоритма расчета важности переменных, или useModel = FALSE , то использовать непераметрическую или параметрическую оценку важности переменных. scale - надо ли шкалировать значения важности переменных в диапазоне от 0 до 100 или нет.

Определим важности переменных в модели m1.pls

Получившиеся значения щкалированы в диапазоне 14, чтобы вывести оригинальные значения зададим параметр scale = FALSE

Универсальные значения важности переменных, можно получить если задать параметр useModel = FALSE или применять функцию varImp к модели, для которой нет собственной реализации расчета важности переменных. В этом случае результаты уже не будут зависеть от самой модели.

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

Если использовать непараметрический подход, то важность переменных в этом случае представляется как величина коэффициента детерминации для локальной регрессии (loess - locally weighted scatterplot smoothing), включающей одну переменную.

Как видно во всех случаях некоторые переменные ранжируются приблизительно одинаково с точки зрения их важности, независимо от выбранного метода оценки важности.

Уровень 4. Использование собственных метрик для оценки качества моделей.

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

Функция trainControl среди прочих имеет параметр summaryFunction , который по умолчанию равен defaultSummary - функция, которая возвращает статистику для кросс-валидации. Можно создать собственную функцию расчета статистических показателей и передать в качестве параметра summaryFunction .

Создадим функцию, которая будет возвращать в качестве статистических показателей величины \( MAE \), \( RMSE \) и \( R^2 \). Функция должна содержать три обязательных параметра. data представляет собой data.frame , который содержит колонки obs и pred , для наблюдаемых значений и значений предсказанных в ходе процедура кросс-валидации. Результатом функции должен быть вектор значений с именами.

Создадим новый объект trainControl , включающий созданную функцию, и построим модель.

В списке результатов появилось новое значение, однако выбор оптимальной модели происходит на основании величины RMSE. Если необходимо использовать для выбора оптимальной модели величину MAE, укажем это в качестве параметра функции train

В этом случае алгоритм выбрал ту же модель с числом компонент 7, но уже на основании величины MAE

Данные для цитирования: . ФОРМИРОВАНИЕ ПРИМЕРОВ ОБУЧАЮЩЕЙ И ТЕСТОВОЙ ВЫБОРКИ НЕЙРОННОЙ СЕТИ НА ПРИНЦИПАХ ПЛАНИРОВАНИЯ ЭКСПЕРИМЕНТОВ // Евразийский Союз Ученых — публикация научных статей в ежемесячном научном журнале. Физико-математические науки. ; ():-.

Получение примеров для обучающей и тестовой выборки является серьезной проблемой построения нейросетевых моделей. В большинстве случаев моделируемый объект неизвестен и требуется выполнение экспериментов для нахождения таких примеров.


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

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

На сегодняшний день не существует строгих рекомендаций по поводу размерности обучающей выборки. Приводятся примерные на оценки количества обучающих пар L обучающей выборки, например [3], , где W — количество настраиваемых параметров сети (количество весов), e — погрешность обучения. В [1] число обучающих пар для многослойных сетей прямого распространения с одним скрытым слоем приблизительно оценивается в зависимости от параметров сети как , где n, p, m - число нейронов в слоях – во входном, в скрытом и в выходном соответственно.

Размерность обучающей выборки определяется различными факторами и не должна быть избыточной. И главным ограничением здесь может являться сложность и высокая стоимость самих экспериментов. Недопустимо становится выполнение опытов в произвольно выбранных точках и в произвольном порядке. Ценность измерений может оказаться невысокой при значительных затратах на их проведение.

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

Для нейронной сети выбранной структуры и размерности сначала необходимо примерно оценить размер обучающей выборки (и, следовательно, количество необходимых опытов). Это позволит определить коэффициент дробности k плана ДФЭ . При числе факторов 2, 3 или 4 это может быть и план ПФЭ , то есть k — 0. Но при большом числе факторов и, особенно, при сложности проведения опытов дробность может составлять 2…3 и выше.

Для ДФЭ определяются основные и дополнительные факторы; задаются значения основных факторов, а значения дополнительных факторов рассчитываются по генерирующим соотношениям [2]. Составляется расширенная матрица плана, которая реализуется, то есть выполняются опыты и находится примеров обучающей выборки.

Полученные примеры используются для обучения нейронной сети. По результатам обучения определяются дальнейшие действия.

  1. Заданная величина ошибки не достигается и сеть обучить не удается.

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

В случае успешного обучения необходима проверка, то есть тестирование сети, для чего создается тестовая выборка. Примеры тестовой выборки получаются также достраиванием плана ДФЭ до плана ДФЭ . Производится тестирование на примерах полученной тестовой выборки. Если тестирование прошло удачно, то сеть обучена и готова к работе. Если тестирование показало плохие результаты, то примеры обучающей и тестовой выборок объединяются в одну обучающую выборку. Обучение продолжается или повторяется заново с удвоенной выборкой. В зависимости от результатов обучения повторяются и последующие действия.

Общий алгоритм последовательного формирования обучающей и тестовой выборок при обучении нейронной сети представлен на рисунке 1.

ФОРМИРОВАНИЕ ПРИМЕРОВ ОБУЧАЮЩЕЙ И ТЕСТОВОЙ ВЫБОРКИ НЕЙРОННОЙ СЕТИ НА ПРИНЦИПАХ ПЛАНИРОВАНИЯ ЭКСПЕРИМЕНТОВ

Рисунок 1. Алгоритм последовательного формирования обучающей и тестовой выборок при обучении нейронной сети

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


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

В этом случае предлагается простое решение.

Факторный план следует понимать как расширение плана ПФЭ за счет нового плана ПФЭ , в котором факторы также варьируются на двух уровнях, но величина этих уровней теперь другая. Вместо уровней -1 и +1 (как раньше) уровни могут быть, например, -0,5 и +0,5 и в дальнейшем, в случае целесообразности, ±0,75, ±0,25. На рисунке 2 показаны возможные уровни варьирования для двух факторов и отмечены точки, в которых выполняются опыты. Цифры, которыми помечены точки, означают порядок выполнения опытов (в соответствии с порядком, который принят в полных факторных экспериментах).


Рисунок 2. Возможные уровни варьирования для двух факторов


Такой подход позволяет экономно проводить эксперимент, в отличии, например, от перехода к планам , в которых факторы варьируются на трех уровнях. Кроме того, такие планы ПФЭ с двумя уровнями варьирования значений факторов, отличных от ±1, сохраняют свои замечательные свойства – свойства ортогональности и симметричности. Это позволяет легко находить параметры регрессионной модели. Регрессионная модель также может быть получена для сравнения с обученной нейросетевой моделью.

Экспериментальная проверка показала, что регрессионная модель в виде полинома первого порядка, построенная по результатам ДФЭ с пренебрежением некоторыми взаимодействиями факторов, и даже модель, построенная по результатам ПФЭ с учетом всех возможных сочетаний факторов, в большинстве случаев обладает меньшей точностью, чем нейронная сеть, обученная на том же экспериментальном материале.

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


И попробуем загнать в модель машинного обучения окружения точек, точнее, цвета тех точек, которые находятся вокруг данной точки. Напомню, что цвет точки – это числа от 0 до 255, для каждой составляющих RGB (Red, Blue, Green).

Давайте изобразим это графически:


В центре колец из циферок – искомая точка, а точки, ее окружающие, подаются на вход модели машинного обучения. Если размер скользящего окна (вот этого квадратика из циферок) равен 5, то входной элемент состоит из 24 точек (сама эта точке в него не входит). Соответственно, на вход подается 72 числа. На выходе – одно число, это составляющая R, G или B анализируемой точки. Соответственно, нам потребуется три модели, для каждого из цветов RGB, которые мы будет обучать и использовать отдельно.

Для проведения данного эксперимента нам понадобятся следующие знания:

  • Работа с картинками (библиотека PIL): Работа с изображениями
  • Работа с таблицами данных (Pandas): Анализ данных. PANDAS
  • Машинное обучение (об этом будет рассказано в рамках данного урока).

Для начала создадим экспериментальную программу, которая предскажет красную составляющую цвета точки. В качестве обучающей выборки используем несколько рандомных точек, в качестве тестовой – другие несколько рандомных точек. В качестве модели машинного обучения был выбран случайный лес (RandomForestClassifier). Итак, вот программа на Python:

Данная программа создает обучающую и тестовую выборку и проводит сравнения того, что предсказала модель с оригиналом.

Вот пример ее вывода:

Обучающая выборка 984

Ср. ошибка: 0.02032520325203252

Тестовая выборка 993

Ср. ошибка: 17.300100704934543

Как видим, на обучающей выборке модель сработала идеально, среднее отклонение почти нуль. На тестовой есть небольшая ошибка. Значение абсолютное, в данном примере оно равно 17 (у вас может получиться другое число, так как данные рандомые, да и сам алгоритм машинного обучения тоже завязан на рандоме, обычно получается от 15 до 20). Много это или мало? Давайте посмотрим.

Вот у нас цвет с красной составляющей 100, остальные по нулям:




Как видим, разница едва заметна.

Но это среднее значения. Где-то может быть отклонение больше 17, а где-то меньше. Более точную картинку даст среднее в купе со среднеквадратичным отклонением, но мы сейчас не будет его считать, если интересно – пусть это будет вашим домашним заданием. А мы сделаем вот что: попробуем загнать на обученные модели всю картинку и посмотрим, какая картинка получиться на выходе.

Для этого воспользуемся следующей программой на Python:

Я настоятельно рекомендую сначала загнать все точки в выборку, и прогнать эту выборку целиком через модели, а потом уже по результатам зажигать точки (как сделано в данном примере). Не предъявлять модели каждую точку по отдельности, это будет работать гораздо медленнее.

Теперь посмотрим на результат:


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

Читайте также: