habr открытый курс машинного обучения
Открытый курс машинного обучения. Тема 1. Первичный анализ данных с Pandas
Открытый курс машинного обучения mlcourse.ai сообщества OpenDataScience – это сбалансированный по теории и практике курс, дающий как знания так и навыки (необходимые, но не достаточные) машинного обучения уровня Junior Data Scientist. Нечасто встретите и подробное описание математики, стоящей за используемыми алгоритмами, и соревнования Kaggle, и примеры бизнес-применения машинного обучения в одном курсе. С 2017 по 2020 годы yorko и большая команда ODS проводили живые запуски курса дважды в год – с домашними заданиями, соревнованиями и общим рейтингом учатсников (имена героев все еще запечатлены тут). Верим, что силами ODS такой формат возродится, а пока, на август 2021, курс – в режиме самостоятельного прохождения.
Опубликован план самостоятельного прохождения курса (англ.) – https://mlcourse.ai/roadmap.
Список статей серии
План этой статьи
1. О курсе
Мы не ставим себе задачу разработать еще один исчерпывающий вводный курс по машинному обучению или анализу данных (т.е. это не замена специализации Яндекса и МФТИ, дополнительному образованию ВШЭ и прочим фундаментальным онлайн- и оффлайн-программам и книжкам). Цель этой серии статей — быстро освежить имеющиеся у вас знания или помочь найти темы для дальнейшего изучения. Подход примерно как у авторов книги Deep Learning, которая начинается с обзора математики и основ машинного обучения — краткого, максимально ёмкого и с обилием ссылок на источники.
Если вы планируете пройти курс, то предупреждаем: при подборе тем и создании материалов мы ориентируемся на то, что наши слушатели знают математику на уровне 2 курса технического вуза и хотя бы немного умеют программировать на Python. Это не жёсткие критерии отбора, а всего лишь рекомендации — можно записаться на курс, не зная математики или Python, и параллельно навёрстывать:
Также про курс рассказано в этом анонсе.
Какое ПО нужно
Для прохождения курса нужен ряд Python-пакетов, большинство из них есть в сборке Anaconda с Python 3.6. Чуть позже понадобятся и другие библиотеки, об этом будет сказано дополнительно. Полный список можно посмотреть в Dockerfile.
Также можно воспользоваться Docker-контейнером, в котором все необходимое ПО уже установлено. Подробности – на странице Wiki репозитория.
2. Домашние задания в курсе
Каждая статья сопровождается домашним заданием в виде тетрадки Jupyter, в которую надо дописать код, и на основе этого выбрать правильный ответ в форме Google. Примеры домашних заданий приведены в статьях серии (в конце).
3. Демонстрация основных методов Pandas
Весь код можно воспроизвести в этом Jupyter notebook.
Основными структурами данных в Pandas являются классы Series и DataFrame. Первый из них представляет собой одномерный индексированный массив данных некоторого фиксированного типа. Второй – это двухмерная структура данных, представляющая собой таблицу, каждый столбец которой содержит данные одного типа. Можно представлять её как словарь объектов типа Series. Структура DataFrame отлично подходит для представления реальных данных: строки соответствуют признаковым описаниям отдельных объектов, а столбцы соответствуют признакам.
Будем показывать основные методы в деле, анализируя набор данных по оттоку клиентов телеком-оператора (скачивать не нужно, он есть в репозитории). Прочитаем данные (метод read_csv ) и посмотрим на первые 5 строк с помощью метода head :
В Jupyter-ноутбуках датафреймы Pandas выводятся в виде вот таких красивых табличек, и print(df.head()) выглядит хуже.
По умолчанию Pandas выводит всего 20 столбцов и 60 строк, поэтому если ваш датафрейм больше, воспользуйтесь функцией set_option :
Каждая строка представляет собой одного клиента – это объект исследования.
Столбцы – признаки объекта.
Название | Описание | Тип |
---|---|---|
State | Буквенный код штата | номинальный |
Account length | Как долго клиент обслуживается компанией | количественный |
Area code | Префикс номера телефона | количественный |
International plan | Международный роуминг (подключен/не подключен) | бинарный |
Voice mail plan | Голосовая почта (подключена/не подключена) | бинарный |
Number vmail messages | Количество голосовых сообщений | количественный |
Total day minutes | Общая длительность разговоров днем | количественный |
Total day calls | Общее количество звонков днем | количественный |
Total day charge | Общая сумма оплаты за услуги днем | количественный |
Total eve minutes | Общая длительность разговоров вечером | количественный |
Total eve calls | Общее количество звонков вечером | количественный |
Total eve charge | Общая сумма оплаты за услуги вечером | количественный |
Total night minutes | Общая длительность разговоров ночью | количественный |
Total night calls | Общее количество звонков ночью | количественный |
Total night charge | Общая сумма оплаты за услуги ночью | количественный |
Total intl minutes | Общая длительность международных разговоров | количественный |
Total intl calls | Общее количество международных разговоров | количественный |
Total intl charge | Общая сумма оплаты за международные разговоры | количественный |
Customer service calls | Число обращений в сервисный центр | количественный |
Целевая переменная: Churn – Признак оттока, бинарный признак (1 – потеря клиента, то есть отток). Потом мы будем строить модели, прогнозирующие этот признак по остальным, поэтому мы и назвали его целевым.
Посмотрим на размер данных, названия признаков и их типы.
Видим, что в таблице 3333 строки и 20 столбцов. Выведем названия столбцов:
Чтобы посмотреть общую информацию по датафрейму и всем признакам, воспользуемся методом info :
Метод describe показывает основные статистические характеристики данных по каждому числовому признаку (типы int64 и float64 ): число непропущенных значений, среднее, стандартное отклонение, диапазон, медиану, 0.25 и 0.75 квартили.
State | International plan | Voice mail plan | |
---|---|---|---|
count | 3333 | 3333 | 3333 |
unique | 51 | 2 | 2 |
top | WV | No | No |
freq | 106 | 3010 | 2411 |
Сортировка
DataFrame можно отсортировать по значению какого-нибудь из признаков. В нашем случае, например, по Total day charge ( ascending=False для сортировки по убыванию):
Сортировать можно и по группе столбцов:
спасибо за замечание про устаревший sort makkos
Индексация и извлечение данных
DataFrame можно индексировать по-разному. В связи с этим рассмотрим различные способы индексации и извлечения нужных нам данных из датафрейма на примере простых вопросов.
14,5% — довольно плохой показатель для компании, с таким процентом оттока можно и разориться.
Воспользуемся этим для ответа на вопрос: каковы средние значения числовых признаков среди нелояльных пользователей?
Скомбинировав предыдущие два вида индексации, ответим на вопрос: сколько в среднем в течение дня разговаривают по телефону нелояльные пользователи?
Какова максимальная длина международных звонков среди лояльных пользователей ( Churn == 0 ), не пользующихся услугой международного роуминга ( ‘International plan’ == ‘No’ )?
В первом случае мы говорим «передай нам значения для id строк от 0 до 5 и для столбцов от State до Area code», а во втором — «передай нам значения первых пяти строк в первых трёх столбцах».
Хозяйке на заметку: когда мы передаём slice object в iloc, датафрейм слайсится как обычно. Однако в случае с loc учитываются и начало, и конец слайса (ссылка на документацию, спасибо arkane0906 за замечание).
State | Account length | Area code | |
---|---|---|---|
0 | KS | 128 | 415 |
1 | OH | 107 | 415 |
2 | NJ | 137 | 415 |
3 | OH | 84 | 408 |
4 | OK | 75 | 415 |
5 | AL | 118 | 510 |
State | Account length | Area code | |
---|---|---|---|
0 | KS | 128 | 415 |
1 | OH | 107 | 415 |
2 | NJ | 137 | 415 |
3 | OH | 84 | 408 |
4 | OK | 75 | 415 |
Если нам нужна первая или последняя строчка датафрейма, пользуемся конструкцией df[:1] или df[-1:] :
Применение функций к ячейкам, столбцам и строкам
Применение функции к каждому столбцу: apply
Применение функции к каждой ячейке столбца: map
Например, метод map можно использовать для замены значений в колонке, передав ему в качестве аргумента словарь вида
Аналогичную операцию можно провернуть с помощью метода replace :
Группировка данных
В общем случае группировка данных в Pandas выглядит следующим образом:
Группирование данных в зависимости от значения признака Churn и вывод статистик по трём столбцам в каждой группе.
Сделаем то же самое, но немного по-другому, передав в agg список функций:
Сводные таблицы
Voice mail plan | No | Yes |
---|---|---|
Churn | ||
0 | 0.602460 | 0.252625 |
1 | 0.120912 | 0.024002 |
Мы видим, что большинство пользователей лояльны и при этом пользуются дополнительными услугами (международного роуминга / голосовой почты).
Давайте посмотрим среднее число дневных, вечерних и ночных звонков для разных Area code:
Total day calls | Total eve calls | Total night calls | |
---|---|---|---|
Area code | |||
408 | 100.496420 | 99.788783 | 99.039379 |
415 | 100.576435 | 100.503927 | 100.398187 |
510 | 100.097619 | 99.671429 | 100.601190 |
Преобразование датафреймов
Как и многое другое в Pandas, добавление столбцов в DataFrame осуществимо несколькими способами.
Например, мы хотим посчитать общее количество звонков для всех пользователей. Создадим объект total_calls типа Series и вставим его в датафрейм:
Добавить столбец из имеющихся можно и проще, не создавая промежуточных Series:
4. Первые попытки прогнозирования оттока
Посмотрим, как отток связан с признаком «Подключение международного роуминга» (International plan). Сделаем это с помощью сводной таблички crosstab, а также путем иллюстрации с Seaborn (как именно строить такие картинки и анализировать с их помощью графики – материал следующей статьи).
International plan | False | True | All |
---|---|---|---|
Churn | |||
0 | 2664 | 186 | 2850 |
1 | 346 | 137 | 483 |
All | 3010 | 323 | 3333 |
Видим, что когда роуминг подключен, доля оттока намного выше – интересное наблюдение! Возможно, большие и плохо контролируемые траты в роуминге очень конфликтогенны и приводят к недовольству клиентов телеком-оператора и, соответственно, к их оттоку.
Далее посмотрим на еще один важный признак – «Число обращений в сервисный центр» (Customer service calls). Также построим сводную таблицу и картинку.
Customer service calls | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | All |
---|---|---|---|---|---|---|---|---|---|---|---|
Churn | |||||||||||
0 | 605 | 1059 | 672 | 385 | 90 | 26 | 8 | 4 | 1 | 0 | 2850 |
1 | 92 | 122 | 87 | 44 | 76 | 40 | 14 | 5 | 1 | 2 | 483 |
All | 697 | 1181 | 759 | 429 | 166 | 66 | 22 | 9 | 2 | 2 | 3333 |
Может быть, по сводной табличке это не так хорошо видно (или скучно ползать взглядом по строчкам с цифрами), а вот картинка красноречиво свидетельствует о том, что доля оттока сильно возрастает начиная с 4 звонков в сервисный центр.
Churn | 0 | 1 | All |
---|---|---|---|
Many_service_calls | |||
0 | 2721 | 345 | 3066 |
1 | 129 | 138 | 267 |
All | 2850 | 483 | 3333 |
Объединим рассмотренные выше условия и построим сводную табличку для этого объединения и оттока.
Значит, прогнозируя отток клиента в случае, когда число звонков в сервисный центр больше 3 и подключен роуминг (и прогнозируя лояльность – в противном случае), можно ожидать около 85.8% правильных попаданий (ошибаемся всего 464 + 9 раз). Эти 85.8%, которые мы получили с помощью очень простых рассуждений – это неплохая отправная точка (baseline) для дальнейших моделей машинного обучения, которые мы будем строить.
В целом до появления машинного обучения процесс анализа данных выглядел примерно так. Прорезюмируем:
5. Домашнее задание №1
Далее курс будет проводиться на английском языке (статьи на Медиуме тоже есть). Следующий запуск – 1 октября 2018 г.
Для разминки/подготовки предлагается поанализировать демографические данные с помощью Pandas. Надо заполнить недостающий код в Jupyter-заготовке и выбрать правильные ответы в веб-форме (там же найдете и решение).
6. Обзор полезных ресурсов
Статья написана в соавторстве с yorko (Юрием Кашницким).
Материалы открытого курса OpenDataScience и Mail.Ru Group по машинному обучению и новый запуск
Недавно OpenDataScience и Mail.Ru Group провели открытый курс машинного обучения. В прошлом анонсе много сказано о курсе. В этой статье мы поделимся материалами курса, а также объявим новый запуск.
UPD: теперь курс — на английском языке под брендом mlcourse.ai со статьями на Medium, а материалами — на Kaggle (Dataset) и на GitHub.
Кому не терпится: новый запуск курса — 1 февраля, регистрация не нужна, но чтоб мы вас запомнили и отдельно пригласили, заполните форму. Курс состоит из серии статей на Хабре (Первичный анализ данных с Pandas — первая из них), дополняющих их лекций на YouTube-канале, воспроизводимых материалов (Jupyter notebooks в github-репозитории курса), домашних заданий, соревнований Kaggle Inclass, тьюториалов и индивидуальных проектов по анализу данных. Главные новости будут в группе ВКонтакте, а жизнь во время курса будет теплиться в Slack OpenDataScience (вступить) в канале #mlcourse_ai.
План статьи
Чем курс отличается от других
1. Не для новичков
Часто вам будут говорить, что от вас ничего не требуется, через пару месяцев вы станете экспертом анализа данных. Я все еще помню фразу Andrew Ng из его базового курса «Machine Learning»: «вы не обязаны знать, что такое производная, и сейчас вы разберетесь, как работают алгоритмы оптимизации в машинном обучении». Или «вы уже почти что эксперт анализа данных» и т.д. При всем безмерном уважении к профессору — это жесткий маркетинг и желтуха. Вы не разберетесь в оптимизации без знания производных, основ матана и линейной алгебры! Скорее всего вы не станете даже Middle Data Scientist, пройдя пару курсов (включая наш). Легко не будет, и больше половины из вас отвалится примерно на 3-4 неделе. Если вы wannabe, но не готовы с головой погрузиться в математику и программирование, видеть красоту машинного обучения в формулах и добиваться результатов, печатая десятки и сотни строк кода — вам не сюда. Но надеемся, вам все же сюда.
В связи с вышесказанным мы указываем порог вхождения — знание высшей математики на базовом (но не плохом) уровне и владение основами Python. Как подготовиться, если этого у вас пока нет, подробно описано в группе ВКонтакте и тут под спойлером, чуть ниже. В принципе можно пройти курс и без математики, но тогда см. следующую картинку. Конечно, насколько дата саентисту нужно знать математику — это холивар, но мы тут на стороне Андрея Карпатого, Yes you should understand backprop. Ну и вообще без математики в Data Science — это почти как сортировать пузырьком: задачу, может, и решишь, но можно лучше, быстрее и умнее. Ну и без математики, конечно, не добраться до state-of-the-art, а за ним следить очень увлекательно.
Математика
Python
2. Теория vs. Практика Теория и Практика
Курсов по машинному обучению полно, есть действительно классные (как специализация «Машинное обучение и анализ данных»), но многие сваливаются в одну из крайностей: либо слишком много теории (PhD guy), либо, наоборот, практика без понимания основ (data monkey).
Мы ищем оптимальное соотношение: у нас много теории в статьях на Хабре (показательна 4-я статья про линейные модели), мы пытаемся ее преподнести максимально понятно, на лекциях излагаем еще более популярно. Но и практики море — домашние задания, 4 соревнования Kaggle, проекты… и это еще не все.
3. Живое общение
Чего не хватает в большинстве курсов — так это живого общения. Новичкам порой нужен всего один короткий совет, чтобы сдвинуться с места и сэкономить часы, а то и десятки часов. Форумы Coursera обычно к какому-то моменту вымирают. Уникальность нашего курса — активное общение и атмосфера взаимоподдержки. В Slack OpenDataScience при прохождении курса помогут с любым вопросом, чат живет и процветает, возникает свой юмор, кто-то кого-то троллит… Ну а главное, что авторы домашних заданий и статей — там же в чате — всегда готовы помочь.
4. Kaggle в действии
Из паблика ВКонтакте «Мемы про машинное обучение для взрослых мужиков».
Соревнования Kaggle — отличный способ быстро прокачаться в практике анализ данных. Обычно в них начинают участвовать после прохождения базового курса машинного обучения (как правило, курса Andrew Ng, автор, безусловно, харизматичен и прекрасно рассказывает, но курс уже сильно устарел). У нас в течение курса будет предложено поучаствовать аж в 4 соревнованиях, 2 из них — часть домашнего задания, надо просто добиться определенного результата от модели, а 2 других — уже полноценные соревнования, где надо покреативить (придумать признаки, выбрать модели) и обогнать своих товарищей.
5. Бесплатно
Ну тоже немаловажный фактор, чего уж там. Сейчас на волне распространения машинного обучения вы встретите немало курсов, предлагающих обучить вас за весьма кругленькую компенсацию. А тут все бесплатно и, без ложной скромности, на очень достойном уровне.
Материалы курса
Здесь мы вкратце опишем 10 тем курса, чему они посвящены, почему без них не может обойтись курс базового машинного обучения, и что нового мы внесли.
Тема 1. Первичный анализ данных с Pandas. Статья на Хабре
Хочется сразу начать с машинного обучения, увидеть математику в действии. Но 70-80 % времени работы над реальным проектом — это возня с данными, и тут Pandas очень хорош, я его использую в работе практически каждый день. В статье описываются основные методы Pandas для первичного анализа данных. Затем мы анализируем набор данных по оттоку клиентов телеком-оператора и пытаемся «прогнозировать» отток без всякого обучения, просто опираясь на здравый смысл. Недооценивать такой подход ни в коем случае нельзя.
Тема 2. Визуальный анализ данных c Python. Статья на Хабре
Роль визуального анализ данных сложно переоценить — так создаются новые признаки, ищутся закономерности и инсайты в данных. К.В. Воронцов приводит пример, как именно благодаря визуализации догадались, что при бустинге классы продолжают «раздвигаться» по мере добавления деревьев, и потом уже этот факт был доказан теоретически. В лекции мы рассмотрим основные типы картинок, которые обычно строят для анализа признаков. Также обсудим то, как вообще подглядеть в многомерное пространство — c помощью алгоритма t-SNE, который порой помогает рисовать вот такие елочные игрушки.
Тема 3. Классификация, деревья решений и метод ближайших соседей.
Статья на Хабре
Тут мы начнем говорить про машинное обучение и про два простых подхода к решению задачи классификации. Опять же, в реальном проекте надо начинать с самых простых подходов, и именно деревья решений и метод ближайших соседей (а также линейные модели, следующая тема) стоит попробовать в первую очередь после эвристик. Затронем важный вопрос оценки качества моделей и кросс-валидацию. Подробно обсудим плюсы и минусы деревьев и метода ближайших соседей. Статья длинная, но в особенности деревья решений заслуживают внимания — именно на их основе выстроены случайный лес и бустинг — алгоритмы, которые вы наверное будете больше всего использовать на практике.
Тема 4. Линейные модели классификации и регрессии.
Статья на Хабре
Эта статья уже будет размером с небольшую брошюру и недаром: линейные модели — самый широко используемый на практике подход к прогнозированию. Эта статья — как наш курс в миниатюре: много теории, много практики. Мы обсудим, каковы теоретические предпосылки метода наименьших квадратов и логистической регрессии, а также в чем плюсы практического применения линейных моделей. Отметим при этом, что излишнего теоретизирования не будет, подход к линейным моделям в машинном обучении отличается от статистического и эконометрического. На практике мы применим логистическую регрессию уже ко вполне реальной задаче идентификации пользователя по последовательности посещенных сайтов. После четвертого домашнего задания отсеется много народу, но если вы его все-таки сделаете, то будете иметь уже очень неплохое представление о том, какие алгоритмы используются в production-системах.
Тема 5. Композиции: бэггинг, случайный лес. Статья на Хабре
Тут опять и теория интересная, и практика. Мы обсудим то, почему для моделей машинного обучения работает «мудрость толпы», и много моделей работают лучше, чем одна, даже лучшая. А на практике покуртим случайный лес (композицию многих деревьев решений) — то, что стоит попробовать, если вы не знаете, какой алгоритм выбрать. Подробно обсудим многочисленные плюсы случайного леса и области его применения. И, как всегда, не без недостатков: все же есть ситуации, когда линейные модели будут работать лучше и быстрее.
Тема 6. Построение и отбор признаков. Приложения в задачах обработки текста, изображений и геоданных. Статья на Хабре, лекция про регрессию и регуляризацию.
Тут план статей и лекций немного расходится (всего один раз), уж слишком велика четвертая тема линейных моделей. В статье описаны главные подходы к извлечению, преобразованию и построению признаков для моделей машинного обучения. Вообще это занятие, построение признаков, — наиболее творческая часть работы Data Scientist-а. И конечно, важно знать, как работать с различными данными (текстами, картинками, геоданными), а не просто с готовым датафреймом Pandas.
На лекции опять обсудим линейные модели, а также основную технику настройки сложности ML-моделей — регуляризацию. В книге «Deep Learning» даже ссылаются на одного известного товарища (лень лезть за пруф-линком), который утверждает, что вообще «все машинное обучение — суть регуляризация». Это, конечно, преувеличение, но на практике, чтобы модели хорошо работали, их надо настраивать, то есть именно правильно использовать регуляризацию.
Тема 7. Обучение без учителя: PCA, кластеризация. Статья на Хабре
Тут мы переходим к обширной теме обучения без учителя — это когда есть данные, а вот целевого признака, который хотелось бы прогнозировать — вот его нет. Таких неразмеченных данных пруд пруди, и надо уметь и из них извлекать пользу. Мы обсудим только 2 типа задач — кластеризацию и снижение размерности. В домашнем задании вы будете анализировать данные с акселерометров и гироскопов мобильных телефонов и пытаться по ним кластеризовать носителей телефонов, выделять типы активностей.
Тема 8. Обучение на гигабайтах c Vowpal Wabbit. Статья на Хабре
Теория тут — это разбор стохастического градиентного спуска, именно этот метод оптимизации позволил успешно обучать и нейронные сети, и линейные модели на больших обучающих выборках. Тут мы также обсудим, что делать, когда признаков становится уж слишком много (трюк с хэшированием значений признаков) и перейдем к Vowpal Wabbit — утилитке, с помощью которой можно за считанные минуты обучить модель на гигабайтах данных, да порой еще и приемлемого качества. Рассмотрим много приложений в различных задачах — классификации коротких текстов, а также категоризации вопросов на StackOverflow. Пока перевод именно этой статьи (в виде Kaggle Kernel) служит примером того, как мы будем подавать материал на английском на Medium.
Тема 9. Анализ временных рядов с помощью Python. Статья на Хабре
Тут обсудим различные методы работы с временными рядами: какие этапы подготовки данных необходимы для моделей, как получать краткосрочные и долгосрочные прогнозы. Пройдемся по различным типам моделей, начиная от простых скользящих средних и заканчивая градиентным бустингом. Также посмотрим на способы поиска аномалий во временных рядах и поговорим о достоинствах и недостатках этих способов.
Тема 10. Градиентный бустинг. Статья на Хабре
Ну и куда без градиентного бустинга… это и Матрикснет (поисковая машина Яндекса), и Catboost — новое поколение бустинга в Яндексе, и поисковик Mail.Ru. Бустинг решает все три основные задачи обучения с учителем — классификации, регрессии и ранжирования. И вообще его хочется назвать лучшим алгоритмом, и это близко к правде, но лучших алгоритмов не бывает. Но если у вас не слишком много данных (влезает в оперативную память), не слишком много признаков (до нескольких тысяч), и признаки разнородные (категориальные, количественные, бинарные, и т.д.), то, как показывает опыт соревнований Kaggle, почти наверное лучше всего в вашей задаче себя проявит градиентный бустинг. Поэтому недаром появилось столько крутых реализаций — Xgboost, LightGBM, Catboost, H2O.
Опять же, мы не ограничимся мануалом «как тюнить иксжбуст», а подробно разберемся в теории бустинга, а затем рассмотрим его на практике, в лекции дойдем и до Catboost. Тут заданием будет побить бейзлайн в соревновании — это даст неплохое представление о методах, работающих во многих практических задачах.
Подробнее о новом запуске
Курс стартует 5 февраля 2018 года. В течение курса будут:
Как подключиться к курсу?
Формальной регистрации не нужно. Просто делайте домашки, участвуйте в соревнованиях, и мы учтем вас в рейтинге. Тем не менее, заполните этот опрос, оставленный e-mail будет вашим ID во время курса, заодно напомним о старте ближе к делу.
Площадки для обсуждения
Удачи! Напоследок хочу сказать, что все получится, главное — не бросайте! Вот это «не бросайте» вы сейчас пробежали взглядом и скорее всего даже не заметили. Но задумайтесь: именно это главное.