Как я решал соревнование по машинному обучению data-like

Привет, Хабр. Недавно прошло соревнование от Тинькофф и McKinsey. Конкурс проходил в два этапа: первый — отборочный, в kaggle формате, т.е. отсылаешь предсказания — получаешь оценку качества предсказания; побеждает тот, у кого лучше оценка. Второй — онсайт хакатон в Москве, на который проходит топ 20 команд первого этапа. В этой статье я расскажу об отборочном этапе, где мне удалось занять первое место и выиграть макбук. Команда на лидерборде называлась «дети Лёши».

Соревнование проходило с 19 сентября до 12 октября. Я начал решать ровно за неделю до конца и решал почти фулл-тайм.

Краткое описание соревнования:

Летом в банковском приложении Тинькофф появились stories (как в Instagram). На story можно отреагировать лайком, дизлайком, скипнуть или просмотреть до конца. Задача предсказать реакцию пользователя на story.

Соревнование по большей части табличное, но в самих историях есть текст и картинки.

План рассказа

Метрика

Прогноз реакции может принимать значение от -1 до 1 включительно — чем оно ближе к 1, тем выше вероятность получить лайк. А при значении -1 лучше убрать эту историю долой с глаз пользователя.

Для проверки точности решений используется формула, нормированная на максимально возможный результат:

$\begin{array}{l}{\text { weight (event) }=\left\{\begin{array}{ll}{-10} & {\text { dislike }} \\ {-0.1} & {\text { skip }} \\ {0.1} & {\text { view }} \\ {0.5} & {\text { like }}\end{array}\right.} \\[15pt] {\text { Metric}\left(y_{\text {pred}}\right)=\sum_{i=1}^{n}\left(\text {weight}\left(\text {event}_{i}\right) \cdot y_{\text {pred, } i}\right)}\end{array}$

Какие данные есть:

  • Базовая информация о пользователе
  • Транзакции пользователей
  • Информация о story (json, из которого можно её сконструировать)
  • История реакций пользователей на stories.

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

Информация о пользователе

что есть изначально:

  • id пользователя
  • анонимизированные продукты банка, которые пользователь открыл (OPN), пользуется (UTL) или закрыл (CLS)
  • пол, бинаризованный возраст, семейное положение, время первого захода в приложение
  • job_title — то, что люди сами пишут о себе
  • job_position_cd — должность человека, как одна из 22 категорий

как фичи используем всё вышеперечисленное кроме job_title, т.к. предполагаем, что job_position_cd нормально описывает должность человека.

Транзакции

что есть изначально:

  • id пользователя
  • день, месяц транзакции
  • сумма транзакции (бинаризована с шагом 250)
  • merchant_id — внутренний банковский id кассового аппарата. В дальнейшем не используется.
  • merchant_mcc

MCC — Merchant category code. Это стандартизованный код услуги, которую предоставляет получатель. Эта информация открыта, вот расшифровка. Эти коды можно удобно разбить на категории, например: entertainment, hotels и т.п.

Для каждого customer_id сопоставим следующие фичи:

  • посчитаем сумму расходов, средний чек, стандартное отклонение
  • количество транзакций
  • разобъём mcc коды на 20 категорий, посчитаем сколько человек потратил денег на эту категорию. Получим 20 фичей
  • ещё 20 фичей получим, разделив расходы в категории на сумму расходов. Т.е. получим процент денег потраченных на категорию.

Stories

Всего историй у нас 959.
что есть изначально:

  • id истории
  • json истории.

выглядит json подобным образом:

Это такое дерево элементов, где каждый элемент описывается ключами: [‘guid’, ‘type’, ‘description’, ‘properties’, ‘content’]. В ‘content’ лежит список дочерних элементов. История состоит из страниц. На страницу накиданы фон, текст, картинки. Конструктора историй у нас не было, а самому отрисовать всё это достаточно сложно и не факт, что значительно поможет в дальнейшем.

Регулярками вытащим весь текст и соответствующий размер шрифта. Извлечём следующие фичи:

  • количество страниц, ссылок, всего элементов
  • средний размер шрифта текста
  • количество текстовых элементов
  • «объём текста» — эвристика, чтобы взвешенно учесть длину текста в зависимости от размера шрифта.

Код подсчёта объёма

def get_text_amount(all_text, font_sizes):     assert len(all_text) == len(font_sizes)     lengths = np.array(list(map(len, all_text)))     sizes = (np.array(font_sizes) / 100)**2     return (lengths * sizes).sum()

  • Возьмём теперь весь текст, с помощью dostoevsky определим семантику текста: [‘neutral’, ‘negative’, ‘skip’, ‘speech’, ‘positive’]. И добавим это в качестве 5 фичей

Реакции

что есть изначально:

  • id пользователя и истории
  • время
  • реакция

Обработаем время и как фичи добавим:

  • день недели
  • час, минута

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

Какую задачу мы решаем и как формировать предсказание?

Лучший подход, который использовал весь топ, следующий: cведём задачу к многоклассовой классификации, т.е. предсказываем вероятность каждой реакции. Считаем матожидание оценки $E_i$ для данной истории $i$:

$E_i = -10 \cdot P_i(dis) - 0.1 \cdot P_i(skip) + 0.1 \cdot P_i(view) + 0.5 \cdot P_i(like)$

Бинаризуем $E_i$: $\: a_i = sign(E_i)$
$a_i$ — наш ответ для объекта $i$, который может принимать значение $\pm 1$

Модель

С самого начала и до конца я использовал CatBoost. Обусловлено это тем, что CatBoost из коробки строит полезные статистики для категориальных фичей. А статистика по пользователю — то, насколько он склонен к каким реакциям, и статистика по истории — то, как чаще всего не неё реагируют, являются самыми сильными фичами в этой задаче.

Как CatBoost работает с категориальными фичами хорошо объяснено в документации.
TLDR:

  • генерирует несколько перестановок данных
  • идёт по-порядку и строит mean target encoding (mte) по тем объектам, которые он уже видел

кратко про mte на нашем примере

берём значение признака, например, один из customer_id, считаем процент случаев, когда этот customer отреагировал лайком, дизлайком, скипнул или просмотрел. Получим 4 числа. Заменяем customer_id на эти 4 числа и используем их как признаки. Делаем так для каждого customer_id.

Текущий результат

С текущими фичами, с неоптимизированным катбустом, на публичном лидерборде я занимал на тот момент 11 место с результатом 0.31209

Киллер фичи

В какой-то момент появилась гипотеза, что приложение может показывать истории чаще или реже в зависимости от того, как пользователь отреагировал на неё ранее. Давайте тогда добавим фичи, которые будут говорить:

  • сколько раз пользователь видел соответствующую историю в прошлом/будущем, в течение месяца/дня/часа/всего
  • время, которое прошло с последнего просмотра этой же истории
  • время, через которое пользователь в следующий раз посмотрит эту же историю
  • на самом деле у пользователя в одну секунду загружается сразу несколько историй, обычно около 5-7. Назовём этот набор историй группой. Я добавил это количество историй в группе как фичу, что дало большой прирост качества.

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

Итак, сказано — сделано. Получили 0.35657 на лидерборде.

Оптимизация модели

Перебирал параметры я с помощью байесовской оптимизации

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

Выдержка из документации

Assume that the objects in the training set belong to two categorical features: the musical genre (“rock”, “indie”) and the musical style (“dance”, “classical”). These features can occur in different combinations. CatBoost can create a new feature that is a combination of those listed (“dance rock”, “classic rock”, “dance indie”, or “indie classical”).

Интересные наблюдения

  1. CatBoost можно обучать на GPU, это заметно ускоряет обучение, но также вводит много ограничений, особенно касательно категориальных фичей. В этой задаче обучение на GPU давало результат сильно хуже, чем на CPU.

  2. Важность фичей по мнению CatBoost. Во многом названия фичей говорят сами за себя, но некоторые, не самые очевидные, из топа, я поясню:

    • unique_in_group — количество историй в группе. (Внутри группы они всегда уникальные, просто на момент создания фичи я этого не знал)
    • stories_at_all — количество историй, которое просмотрел человек и в будущем, и в прошлом.
    • this_story_before_counter — сколько раз человек посмотрел эту историю ранее.
    • text_amount — та эвристика с объёмом текста.
    • group_number — порядковый номер группы.
    • nearest_story_seconds_before/after — по существу это время до показа следующей группы.

      Картинка кликабельна.

  3. Давайте посмотрим на распределение реакций с течением времени:

    Т.е. в какой то момент распределение по реакциям сильно меняется.

    Далее хочется получить какое-нибудь подтверждение, что на тесте распределение такое же, как и в конце тренировочной выборки. Зашлём как предсказание все единички, получим результат 0.00237. Предскажем все единички на последней части трейна — получим около 0.009, на первой части — около -0.22. Значит распределение на тесте скорее всего такое же, как в конце трейна и точно не похоже на основную часть. Отсюда рождается гипотеза, что если подправить распределение $\pm 1$ в наших предсказаниях, то результат на лидерборде сильно улучшится, т.к. распределения на трейне и на тесте отличаются.

Трешхолд предсказаний

На последнем шаге получения итоговых предсказаний добавим трешхолд: $\: a_i = sign(E_i + \text{threshold})$

В последней модели у меня было что-то около 66% единичек, если бинаризовать с трешхолдом равным 0. Оказалось, что действительно, уменьшение количества +1 давало сильный прирост качества. Оценивались только последние 3 посылки, поэтому я заслал предсказания лучшей модели с разными трешхолдами так, чтобы процент плюс единичек был примерно 62, 58 и 54.

По итогу на публичном лидерборде мой лучший результат был 0.37970.

Результаты соревнования

про публичный/приватный лидерборд

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

В конце соревнования на публичном лидерборде положение было таким:

  1. 0.382 — ЗдесьМоглаБытьВашаРеклама
  2. 0.379 — дети Лёши
  3. 0.372 — Gardeners
  4. 0.35 — lazy&akulov

На приватном лидерборде, по которому считались итоговые результаты, мне повезло и ребята по какой-то причине с первого места упали на 4ое. Вот итоговое положение.

  1. 0.45807 дети Лёши
  2. 0.45264 Gardeners
  3. 0.44136 Zhuk
  4. 0.43704 ЗдесьМоглаБытьВашаРеклама
  5. 0.43474 lazy&akulov

Что не сработало

  1. Я пробовал переводить весь текст из истории в вектор с помощью fasttext, затем кластеризовал векторы и использовал номер кластера как категориальную фичу. Эта фича была топ 3 (после story_id и customer_id) в feature importance CatBoost’а, но почему-то стабильно и значительно ухудшала результат на валидации.
  2. Благодаря кластерам можно было найти истории, которые относились к чемпионату мира по футболу и присутствовали только в тренировочной выборке.
    Однако выкидывание таких объектов из датасета не улучшило результат.
  3. по дефолту CatBoost генерирует случайные перестановки объектов и считает по ним признаки для категориальных фичей. Но можно сказать катбусту, что у нас есть время в данных — has_time = True. Тогда он будет идти по порядку, не перемешивая датасет. В данной задаче, несмотря на то, что время у нас действительно есть, результат с has_time был стабильно хуже.

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

  4. Была идея назначать больший вес объектам в конце трейна, т.е. чтобы больше учитывать объекты с правильным распределением реакций. Но и на валидации, и на публичном лидерборде это давало результат хуже.
  5. Можно учитывать разные реакции с разным весом при обучении. Хотя у меня это результат не улучшало, некоторым командам это помогло.

Выводы

Соревнование получилось интересным, поскольку объединяло много компонентов, таких как табличные данные, тексты и картинки. Было много пространства для исследований, много с чем ещё можно было бы экспериментировать. В общем, скучать не пришлось.

Спасибо организаторам конкурса!

Весь код выложен на гитхабе.


ссылка на оригинал статьи https://habr.com/ru/company/ods/blog/475182/

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

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

Сам я в этой тематике с 2016 года. На тот момент прошёл всего год с момента старта моей компании по разработке VR/AR-решений и, конечно же, я старался присутствовать на всех мероприятиях в качестве эксперта по этому направлению. Подтверждённых экспертов тогда было совсем немного, так что покатался я из города в город немало на мероприятиях разной тематики: от форума Минэкономразвития в Самарской области до до курсов переподготовки библиотекарей в Академии искусств в Барнауле.

В итоге на одном из мероприятий мы познакомились с одной чудесной девчулей из Второй новосибирской гимназии, которая пригласила меня на встречу с директором. В итоге мне внезапно предложили… вести курс по разработке VR/AR приложений для школоты. Для меня это был интересный вызов, но и вместе с тем очень сложный. Методики толком нет, Google и Oculus только-только привели свои SDK более менее в порядок без глобальных косяков, чтобы джуниор мог с ними совладать, я сам человек без педагогического опыта. Вообщем, мне пришлось действовать на ощупь и методом тыка.

В итоге это меня привело к написанию серии методичек по разработке космического симулятора в виртуальной реальности для Google Cardboard и по разработке анимационного мультфильма в виртуальной реальности на базе Oculus SDK для начинающих; по разработке RPG-игры в дополненной реальности для продолжающих; и по разработке стратегии с использованием OpenVR для углублённого изучения. Ну и как-то всё пошло-поехало, завертелось, в итоге сейчас преподаю в гимназии, Новосибирском государственном университете для магистрантов и в паре школ программирования.

При этом это всё занимает у меня примерно 12 часов в неделю, а в остальном я продолжаю трудится над проектами в своей компании. Снисходительно называю себя «училкой», ибо так и не могу принять себя, как состоявшегося педагога, несмотря на фактический трёхлетний опыт и достижения, вроде победы моего ученика в прошлом году на всероссийской олимпиаде НТИ по профилю виртуальной реальности.

Теперь, собственно, к главной мысли, о чём хотелось бы написать. Мой старт обучения пришёлся на учеников 6-7 классов. Я думал: «Для меня не составило труда научиться, так в чём же может быть загвоздка?». На старте обучить быстрой сборке с использованием ассетов, конечно, не составляет никакого труда разобраться. Стоит, конечно оговориться, что говорю я о разработке в редакторе Unity 3D, Однако, когда мы полезли вглубь, делая интерактивные сценарии на скриптах, я столкнулся, объясняя вычисление дистанции между векторами, с тем, что мои ученики ещё даже не прошли понятие квадратного корня (я был уверен, что его проходят ещё в классе третьем). Так что, параллельно с основным материалом, мне пришлось объяснять и основы математики. А видели бы вы глаза тех, кто уже изучил квадратные корни, и вдруг сталкивается с поворотами в 3D-пространстве с использованием мнимых чисел.

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

Начать с того, что AR/VR-разработка — это не какой-то отдельный конь в вакууме. На самом деле, это направление строится из трёх ключевых тем: изучение интерфейса редактора и базовое понимание основ работы, 3D-моделирование и разработка программного обеспечения. Поэтому в идеале курс может выдвигать требования для слушателей: знание геометрии, знание основ компьютерной графики, знание C#. Тогда основной задачей обучения будет всего лишь изучение интерфейса редактора и разбор Unity API. В этом отношении мне нравится этот год работы в Новосибирском государственном университете, потому что у меня есть отдельная группа ребят, которые обладают необходимыми компетенциями, и мы можем изучать по настоящему интересные и сложные вещи, вроде создания произвольной геометрии, программирования шейдеров и Entity Component System.

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

Таким образом, задача создания курса по разработке VR/AR в школе на самом деле становится задачей по организации комплекса курсов, на котором мы сначала изучаем интерфейс редактора Unity, а только затем мы переходим к изучению программирования на языке C# с параллельным разбором основ математики и геометрии (зачастую опережающим школьную программу).

И тут есть опять же интересный вопрос: а какая основная задача курса? Например, на Олимпиаде НТИ основные задачи связаны непосредственно алгоритмами и классическими задачами информатики. Практических кейсов, связанных с AR/VR там практически нет. Это прямо боль. Едет человек, который отлично знает синтаксис языка, который знает нужные фреймворки и может сварганить небольшую игру в VR за сутки, а там ему дают задачи из классики информатики и единственно, что там от AR/VR — это камера.

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

Поэтому тут возникает необходимость в том, чтобы ещё и смежные предметы согласовывали программы между собой. Например, во многих школах сейчас, согласно тренду, идёт переключение с изучения Pascal на Python. Но Python и C# — это разные языки, как английский и иврит. Конечно, есть полиглоты, для которых это не проблема. Однако, на мой взгляд, в школах удобнее ввести разделение на языковые группы, чтобы дети могли изучать язык по желанию, как это сделано с иностранными языками. Вместо принципа «один язык для всех» можно предложить детям ветви развития. Например, хочешь в веб-разработку — php+javascript, хочешь в игровую разработку и создание AR/VR — изучай си и его производные, ничего не хочешь — иди изучай python, как крайний случай (шучу конечно).

И под конец в дело вступают ожидания по отдаче от курса. Например, заместитель директора фантазирует: «А теперь пусть они сделают бегемотов, пляшущих на по коридорам нашего учебного заведения?». Я думаю, знающие люди уже уловили проблему в этом техзадании: где взять контент, да ещё с чёткой заданной анимацией? Фактически в рамках вышеописанного курса мы готовим программистов, а не 3D-моделеров и аниматоров, Дайте им модель с анимацией с какого-нибудь Mixamo и дайте им модели окружения, тогда они сваяют из них какую-нибудь страшненькую сцену. Потому что они не художники и им глубоко накласть на красоту пользовательского интерфейса и цветовую гамму. У них всё будет работать и этого достаточно.

Тут есть ещё одна проблема: на курс по разработке AR/VR приходят не только желающие программировать, а те, кто хочет сделать что-то красивое. И им как раз глубоко похрену на вот эту всю геометрию и программирование. Они художники и хотят реализоваться в этой сфере. Более того, их роль не менее важна, чем роль программистов, но сегодня в основном все учебные программы касаются именно разработки софта а не создания контента.

Поэтому на самом деле, если руководство образовательного заведения хочет похвастать комплексным решением в качестве итога работы своей образовательной программы, нужно создавать ещё отдельный курс для 3D-моделеров и аниматоров. В этом отношении я, конечно, рекомендую учебным заведениям выбрать Blender в качестве основной программы, поскольку она бесплатна. При разговоре об этом направлении многие директора школ сразу с гордостью заявляют, что у них уже есть направление 3D-моделирования, где дети создают оборудование. И каждый раз я пытаюсь донести мысль о том, что инженерное моделирование, архитектурное моделирование и игровое моделирование — это три глобально различающиеся направления со своими инструментами и методиками.

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

p.s. Методичками готов поделиться, но не за бесплатно. За бесплатно мне мама не разрешает. А вы же знаете этих еврейских мам.


ссылка на оригинал статьи https://habr.com/ru/post/477886/

work&dev fun(damentals) #0. Что ожидать и как помогать расти разработчику из trainee в уверенного junior?

Что ожидать и как помогать расти разработчику из trainee в уверенного junior?

Уровень разработчика — то чем все привыкли меряться и то за чем все перебегают из компании в компанию.

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

Эта тема меня беспокоит особенно сильно по той причине, что годы опыта все таки кое о чем говорят. Говорят они о количестве времени, во время которого ты работал работу. И чисто статистически верно то, что за m времени может произойти больше факапов и запар, чем за n, при условии что m > n. Вот и все. Об этом говорят года опыта. Это не тот показатель, по которому я буду отсеивать людей с позиций (буду если это сеньор, с 1.5 годами реального опыта), но тот, по которому я буду решать между двумя идентичными кандидатами, если не смогу взять двух.

Так вот, мой любимый тип разработчика — trainee. Это совсем начинающие ребята, не важно какой у них возраст_пол_вероисповедание, по ним с первого дня видно, горят ли у них глаза. Дальше дело техники, как говорит один мой хороший друг: «код можно научить писать и обезьяну», и мы учим… не обезьяну, конечно… а человека. Учим, рассказываем, выгоняем с работы, когда засиживаются, а они любят засиживаться, потому что интересно все. На этом этапе задача разработчика научиться работать с инструментами, понять что вода мокрая, огонь горячий, а стендап от слова «стоять». В каждом языке есть типовое задание. В руби — Хартл и его ака «твиттер». В javascript все дико любят туду листы и всевозможные реализации под тот фреймворк с которым ты работаешь. Если он может это написать по step-by-step гайду, он подходит на трейни. Когда он сможет написать его без step-by-step гайда, можно поговорить про джуна. Я специально сделал здесь упор на step-by-step, потому что не важно сколько у тебя опыта, ты будешь бегать на MDN смотреть порядок параметров в reduce и забывать базовые конструкции.

Дальше Junior — и тут нет резкого перехода. Он плавный. И именно поэтому у нас в компании сделано разделение на Junior Beginner/Junior/Junior Strong. Но это этап, на котором вы можете сразу увидеть, какова культура в вашей команде, я закончу этой мыслью раздел про Junior.

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

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

Ему стоит научиться формулировать мысли, аргументировать позицию, для этого надо начать указывать на вещи, которые неочевидны. Почему я сказал про скрам-покер и top-down. Это отличный способ показать человеку нюансы, на которые вы обращаете внимание из-за уже накопившегося опыта, какие детали вы проясняете, какие спеки вам уже не кажутся расплывчатыми и то КАК вы это делаете.

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

Чем раньше разработчик научиться вниманию к деталям и способам коммуникации по задачам со стейкхолдерами, тем проще ему будет дальше. Потому что проективные коммуникации и разбор непонятного — наш сознательный способ пихнуть себя в неизвестность и получить +1 новый кейс в свой опыт.

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

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

Заканчивая описывать Junior`a, вернемся к культуре команды. На этом уровне человек будет впитывать культуру коммуникации команды, если вы шеймите тестировщиков и считаете их бесполезными, но не отдаете себе в этом отчета, взгляните на джуна и вспомните, был ли он таким пол года/год назад. Вел ли он себя так же по отношению к этим людям? Если «нет» в негативную сторону, то вот вам и звоночек. Он научился этому у вас и вашего окружения. Он еще не может четко сказать, почему что-то не важно, но уже это шеймит. Тем более что все мы уже знаем, каждый этап в разработке приложения важен и какой бы ни была команда, без тестировщика они выпустят продукт хуже или медленнее и дороже.

Изначально я опубликовал статью на Medium, но мне кажется для сегмента, с которым я хочу начать разговор — это плохая площадка. Я опущу часть интро, если хотите пообщаться пишите на @_golubev.

Я дал этой рубрике название work&dev fun(damentals). Потому что работа и разработка — это весело. Но надо учиться фундаментальным вещам. Не важно софт скилы это или хард скилы.
Все, далее описанное, это тот опыт который я приобрел. Он ограничен моим пониманием вещей, происходящих в IT. Процессов, которые тут происходят. Решений, которые принимаются. Это понимание позволило мне из Trainee в в одного их лидов Full-stack направления. Параллельно создать отдел, который специализируется на техническом развитии и мониторинге эмоционального состояния сотрудников, для того, чтобы сделать их работу комфортной и обеспечить их конкретным пониманием того, что именно от них ожидают в компании и проекте.


ссылка на оригинал статьи https://habr.com/ru/post/477894/

Выбираем проводные наушники бюджетного сегмента

Поездка на велосипеде в выходной день или долгая дорога в метро по будням? Тренировка перед важными соревнованиями или спокойный вечер в постели? Может быть, утренняя пробежка или бессонная ночь? В современном мире многие люди не могут представить себе свой досуг без верных друзей – наушников.

image
Казалось бы, что может быть проще? Два динамика, болтающихся на пару со штекером, располагаются в ушной раковине и «кормят» нас творчеством любимых исполнителей. Однако, здесь всё не так просто, как нам с вами хотелось бы. Удовольствие, полученное от прослушивания любимой музыки, напрямую зависит от качества её воспроизведения.

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

Критерии выбора

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

image

1) Удобство

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

Форма ушных раковин у каждого индивидуальна, именно поэтому вопрос удобства довольно субъективный, как говорится, не проверишь – не узнаешь.

2) Качество сборки

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

Несколько лет назад, мой кошелек похудел на пару тройку тысячных купюр взамен на обладание наушниками Monster Turbine. Ни богатая комплектация, ни идеально качество звука, ни громко кричащий с коробки бренд не смогли упасти «ушки» от гибели. Одно неосторожное движение превратило их в беспроводные, оставив коннектор торчать в плеере. Цена – не показатель качества.

3) Частотный диапазон

Человеческое ухо способно воспринимать частоты в диапазоне 20 — 20000 Гц. Начало этого диапазона (низы) – отвечают за низкочастотное звучание, например, басы. Конец же диапазона (верха) отвечает за высокочастотное звучание. Таким образом, приходим к выводу, что хорошие наушники должны работать именно в данном диапазоне, а выход за его рамки сможет услышать разве что человек с очень хорошим слухом или, например, летучая мышь. Ваши музыкальные предпочтения так же должны сыграть роль при выборе наушников. Опираться на глубоко выраженные низы для дальнейшего прослушивания оперного пения – не лучшее решение.
Не стоит полагаться лишь на эти значения. Иногда дешевые аналоги проигрывают более качественным экземпляром в качестве звучания, даже если у них указан более широкий частотный диапазон.

4) Сопротивление

Если глубоко не вдаваться в труды известного физика, то кратко скажу, что сопротивление наушников влияет на качество звучания. Здесь многое зависит от устройства, в связке с которым мы будем использовать наушники. Hi-Fi плеерам и профессиональному оборудованию, как правило, подвластен большой диапазон сопротивления, и его значения не скажутся на качестве звука. Однако, если вы рядовой пользователь, и собираетесь использовать ваши наушники, скажем, в связке с вашим смартфоном или ПК, то универсальный диапазон может колебаться между 10-36 Ом.

5) Цена

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

6) Внешний вид

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

Мой небольшой топ

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

1. Sennheiser CX 300-II

image

Успевшая завоевать сердца многих пользователей модель порадует отличным качеством стереозвука. Замечательно держит как низкие, так и высокие частоты, «серединка» звучит достойно. Отметила для себя приятное звучание инструментальной музыки. Каждый инструмент отчетливо слышно, звук не скатывается в единый ком. Отчетливо слышен каждый барабанный удар, каждый гитарный риф. Качественный глянцевый пластик выглядит довольно стильно и не будет тяготить ухо. Качество сборки так же на высоте, а заводская упаковка придает продукту презентабельный вид. Сомнения вызывает лишь тонкий кабель, который и будет являться ахиллесовой пятой этой модели.

2. JBL T160

image

Данная модель, как и большинство её собратьев, как младших, так и старших, могут впечатлить плотностью и глубиной своего баса. Низкие частоты – это то, без чего вы не можете представить свой плейлист? Тогда «ушки» от JBL, в частности в коллаборации с Harman отлично подойдут в качестве карманного сабвуфера. Вот только верха страдают, инструментальные композиции звучат немного хуже.

3. AKG Y 20

image

Наушники именитой фирмы по праву заслуживают место в топе. Высокая степень детализации звука позволяет проникнуть в «центр» музыкальной композиции, услышать его с первых рядов. Слушая давно заученные наизусть песни, можно открыть в них для себя что-то новое, что прежде не замечал. За высокую детализацию звука придется расплатиться плохим качеством сборки. (Не ожидал от AKG).

4. Xiaomi Mi In-Ear Hybrid Pro

image

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

5. Sony MDR-XB50AP

image

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

Расчёт окончен, вольно. Вносить в топ не стала, но не могу не упомянуть Philips и Panasonic, предлагающие доступные и качественные модели потребителю, но мы сегодня собрались здесь за лучшими из лучших. На этом мой гайд/топ – полукровка подходит к концу.

P.S.

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


ссылка на оригинал статьи https://habr.com/ru/post/477896/

Простые числа — насколько велико наше бессилие?

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

Эту метафору будет проще понять, если провести аналогию с черной дырой: мы не знаем, что находится под ее горизонтом событий, и чтобы это узнать нам нужно придумать способ, как туда добраться. Нечто подобное существует в мире математики. Данное уравнение — это настоящая «формула» простого числа, но чтобы ею пользоваться, нам нужно придумать, как искать подходящие {a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, w, v, x, y, z}.

Черная дыра и данное уравнение — это предельные состояния чего-то реального и абстрактного. И, если о первом существует достаточно догадок и представлений, то о втором, практически ничего не известно. Но, что если это действительно «математическая» черная дыра. Разве вам не интересно что может произойти, если мы попадем

Из чего состоит стена?

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

Самым волнительным является то, что среди этих воображаемых объектов есть особенные — простые числа. Они как детерминированный хаос — предсказуемы и непредсказуемы одновременно, в зависимости от масштаба их рассмотрения. Например, находясь рядом с ними, мы можем заметить, что их количество перед некоторым произвольным числом n, не превзойдет:
image

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

Как появилась стена

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

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

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

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

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

Но мы не можем его решить.

Толщина и прочность стены

Фактически, нам брошен вызов. И нам заранее известно о неизбежном поражении. Возможно, отступление было бы самым благоразумным решением. Но разве нам не нужны эти поражения, чтобы превзойти себя? Давайте хоть чуть-чуть попытаемся его решить.

Взглянем на уравнение еще раз:
image

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

Если решить эту систему уравнений и прибавить 2 к найденному значению k, то мы получим простое число. Но мы можем пойти и другим путем. Взять некоторое простое число, вычесть из него 2, получив таким образом значение k, затем подставить это значение в систему и попытаться найти значения остальных переменных относительно него. Именно этим путем мы и пойдем — попытаемся найти хоть одно решение данного полинома.

На все переменные накладывается два строгих ограничения, они должны быть целыми и не могут быть отрицательными. Если мы примем k=0, то первое простое число, которое мы сможем получить — это 2. Это и будет нашей отправной точкой. После подстановки этого значения в систему уравнений она примет следующий вид:
image

Уравнения (1)-(5) — это линейные уравнения, т.е. степени всех переменных равны 1. Уравнения (6)-(11) имеют очень схожую структуру. Ну и, наконец, уравнения (12)-(14) тоже выделяются в отдельную группу, причем уравнения (13) и (14) похожи друг на друга, как две капли воды.

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

Уравнения (6)-(11) являются модификациями уравнения Пелля:
image

В самом деле, если переписать их вот так:
image

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

Вроде, все просто.

Удар по стене

Мы начнем с уравнений Пелля. Для их решения напишем небольшой скрипт:

from decimal import * getcontext().prec = 50  def peq_dec(N):     n = Decimal(N).sqrt()      a = int(n)     x = n - a     p0, q0 = 1, 0     p1, q1 = int(a), 1      while True:         a = int(1/x)         x = 1/x - a         p_i = a*p1 + p0         q_i = a*q1 + q0         if p_i**2 - N*q_i**2 == 1:             return p_i, q_i             break         p0, q0 = p1, q1         p1, q1 = p_i, q_i

Благодаря ему мы можем сразу найти решение уравнения (10) n = 2, f = 17. Однако, прежде чем двигаться дальше, мы должны кое-что знать об уравнении Пелля.
image

Начнем с того, что n не может быть полным квадратом. К тому же у любого уравнения Пелля существует бесконечное количество решений, среди которых всегда есть тривиальное: x = 1 и y = 0. Каждое последующее решение может быть получено на основе предыдущих, по следующей рекуррентной формуле:
image

Получается, что нам достаточно найти минимальное нетривиальное решение, а все остальные мы можем получить, пользуясь простым алгоритмом. Например, для n = 2 мы можем легко найти такое решение, это x = 3 и y = 2, тогда последующие решения будут выглядеть так:

17, 12 99, 70 577, 408 3363, 2378 19601, 13860 114243, 80782 665857, 470832 3880899, 2744210 22619537, 15994428 131836323, 93222358 768398401, 543339720 4478554083, 3166815962 26102926097, 18457556052 152139002499, 107578520350 886731088897, 627013566048

Стоит ли продолжать дальнейшее решение? Конечно, стоит, но… мы можем попытаться предугадать, что нас ждет впереди.

Давайте пока просто представим, что мы решаем систему уравнений из трех уравнений Пелля следующего вида:
image

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

Решением первого уравнения являются целочисленные точки красной гиперболы, но координата y, каждой такой такой точки присутствует во втором уравнении и может порождать любую гиперболу синего цвета, целочисленные точки которой будут являться решением второго уравнения.

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

Но давайте вернемся к нашей «формуле» простых чисел. Что нас может ждать впереди?

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

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

Опираясь на эти представления мы постараемся улучшить наши алгоритмы. Чтобы сделать это, мы будем брать самые лучшие достижения из многих разделов математики. Постепенно, в алгоритмах будет меньше случайности, но избавиться от нее все равно не получится. Что произойдет потом? Мы вдруг обнаружим, что множество подходящих кандидатов на истинное решение в некоторых местах является парадоксально плотным. Каждая такая плотность будет дарить надежду на то, что где-то в ее центре и спрятан заветный ответ. Муравьям такие плотности будут «казаться» чем-то вроде перевернутой гиперворонки. Мы будем стараться «бить» по их центрам и максимумам. Но что произойдет потом?

Что за стеной?

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

Что может быть под этим горизонтом? Наверное, какая-то математическая сингулярность. Наверное, мы будем знать абсолютно все обо всех множествах, сможем решать любые уравнения и любые задачи. А может новых вопросов и задач больше вообще не будет?

Возникают именно такие мысли. Ну в самом деле, задайте любой вопрос о простых числах и благодаря данному уравнению вы можете получить на него ответ. Бесконечно ли количество простых чисел-близнецов? Решите данное уравнение, сделайте парочку алгебраических выкладок и получите ответ. Каких простых чисел больше, оканчивающихся на 1, 3, 7 или 9? Тот же самый алгоритм: пара-тройка выкладок и подстановка уравнения. Хотите быстро раскладывать числа на простые множители?..

В заключение

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

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

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


ссылка на оригинал статьи https://habr.com/ru/post/477900/