Как я потратил 1 000 000$, чтобы сделать свой переводчик. Часть 3

Неделю назад исполнилось 6 лет, как я открыл фирму, чтобы заниматься машинным переводом. За это время многое произошло. Сейчас в команде 40 человек, 120 серверов и более миллиона пользователей. Но так было не всегда. Помню те времена, когда нас было двое. Мы сидели в маленькой комнате без вентиляции, и каждый час нужно было открывать окна, чтобы совсем не задохнуться. Мы были полны надежд, идей и большого желания создать переводчик, который сможет конкурировать с Google.

В 2017 году у нас был прототип под Linux, который уже что-то переводил. Под iOS собрать тоже удалось. А вот с Windows оказалось не все так просто. Клиенты хотели установку под эту ОС, и нужно было действовать. Для этого пришлось бы перекомпилировать много библиотек с кучей зависимостей, но у меня не было такого опыта. Когда я пошёл на сайты фриланса и разместил несколько объявлений, откликнулось всего нескольких заявок. Cтало ясно, что задача не такая простая, какой кажется.

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

Стоит отметить, что компании, с которыми я говорил, были разными по размеру. Одна из них — известный бренд и лидер в Восточной Европе. Я написал менеджеру по продажам, и со мной обещали связаться. В течение недели я не получил ответа, написал им еще раз, и мне еще раз ответили, что точно со мной свяжутся. Не связались. Ну и ладно!

Потом я начал искать компании попроще, размером до 100 человек, с хорошим портфолио и нишевой экспертизой. Перед началом разговора всегда интересовался, правда ли то, что у них написано на сайте, и что они делают проект любой сложности. Они заверяли, что да, они уже создавали проекты гораздо сложнее моего, и как раз сейчас у них освободился программист, который участвовал в разработке квантового суперкомпьютера. Отлично! Работаем? Не совсем! Сначала нужно погрузиться в тему на 2 недели и заплатить за исследование проблемы по ставке 40$ в час. И только потом я смогу получить ответ, возьмутся они за дело или нет. Предложение заманчивое, но принимать его я, конечно же, не буду.

В одной из компаний сказали, что на оценку задачи им нужно ровно 12 дней и они дадут ответ на 12-й день в 13:45. Такая точность была необычной, но прикольной, ведь деньги за изучение моей задачи они не брали. Ровно через указанное время в 13:45 они позвонили мне на Skype и дали развернутый ответ. Они говорили про нейронные сети, их конфигурации, про библиотеки Windows, Linux, компьютеры, процессоры и молекулы. Ответ был долгим и логичным, но через 20 минут я просто спросил: «Вы сможете это сделать? Ответьте просто “да” или “нет». Конечно же, оказалось «нет».

Где-то на 15-м созвоне мне улыбнулась удача: менеджер компании сказал, что, мол, да, мы делали что-то похожее, у нас есть человек, который выполнит эту задачу за пару дней, но сейчас вечер пятницы и он уже уехал отдыхать. Я жёстко вцепился в последний шанс и начал обещать хорошие деньги, поток заказов и все что угодно, только сделайте мне это. Пусть он посмотрит задание прямо сейчас. “Хорошо, уговорил”. При мне менеджер начал звонить этому разработчику на телефон, параллельно разговор шел по Скайпу.

На заднем фоне были слышны недовольные фразы, немного мата, крики детей и пьяной компании — видно, человек умеет не только работать, но и отдыхать 🙂 Тем не менее, их программист через час уже был со мной в Скайпе и слушал, что требовалось сделать. По его оценкам, эту задачу можно было решить за вечер и 500$, что очень обрадовало меня. Но за вечер это сделано не было. В субботу утром он сказал, что ему потребуется 2 дня. Где-то к среде задача была выполнена, и через месяц мы выпустили первую версию переводчика под Windows. Это был один из немногих позитивных опытов работы.

Когда пришло время делать Android версию, мне казалось я понял, как работать с аутсорсом. На каждое тестовое задание был выделен бюджет в $200 для проверки одной компании. Найдя очередного исполнителя с хорошей репутацией мы созвонились с менеджером и он нам дал бодрого программиста Виталия. За день он качественно сделал главный экран приложения и все вроде ок, но наш тех лид решил спросить почему он выбрал один из подходов. На первых вопросах программист начал сыпаться и замолчал. — Виталий, вы с нами? — поинтересовался я. — Да! И это было все то, что он смог ответить четко. Потом еще немного его мычания и у меня сдали нервы.

-Кто писал этот код, Виталий?

— Я не Виталий!

— А кто?

— Я Дима!

— А где Виталий?

— Ушел домой!

— А почему ты врал?

— Мне директор сказал.

— Дай мне контакты.

— Он рядом тут.

— Дай ему микрофон. Почему вы мне все врете?

— Успокойтесь! Просто у Виталия заболел живот, и я его отпустил. Давайте дадим вам другого разработчика.

— Мне не нужно, давайте просто закроем контракт, и вы получите деньги за тестовое задание.

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

 

Было бы смешно, если бы не было так грустно. Переводчик для Android пришлось делать своими силами, и за год мы его выпустили, покрыв тем самым приложения для iOS, Android, Windows и Mac.

Мистер Хонг и его волшебный скрипт

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

Мистер Хонг выглядел крутым. Хорошие отзывы, классное портфолио и схожие выполненные проекты. У него был понятный английский и понимание задачи. Он был готов приступить немедленно и за $600. Ну что же! За работу!

Через пару дней он скинул скрипт, который работал быстро и качественно. Мы прогнали несколько тестов, спросили, как он решал задачу. Все выглядело разумным. Скрипт был в несколько тысяч строк, и мы подумали, что разберемся с ним потом, когда будет время. Через пару дней скрипт перестал работать. Мы начали искать причины.

Среди большого и запутанного куска кода он вставил вызов API какого-то платного сервиса. То есть, если весь код, кроме этого вызова, убрать, то работа программы не поменяется. Но выяснилось это очень поздно. Закончился бесплатный период того сервиса. А господин Хонг ранее получил уже 5 звезд и хороший отзыв. Молодец, что и говорить! Впредь нужно быть внимательнее.

Пляж, купальник и нейронные сети

Помимо негативного опыта на фриланс-биржах, хватало и позитивных кейсов. Однажды у нас появилась задача улучшить качество распознавателя речи на немецкий язык, и мы начали искать того, кто бы мог в этом помочь. Когда мы разместили проект, на него откликнусь где-то 20 заявок. В основном среди них были профессора, волосатые, бородатые, в очках, с учёной степенью и большими рейтами. В целом, они все были похожи, кроме одной заявки. В профиле было несколько десятков выполненных схожих проектов, причем рейтинг был — все пятёрки, абсолютно все. Ни у кого не было таких мощных оценок. Количество технологий, которые знает человек, было поистине невероятным. Гений просто! Но была одна деталь, которая останавливала меня, чтобы выбрать его как исполнителя. Его профиль назывался «Таня», был написан на транслите, а на фото — девушка в купальнике у моря. По цене за час у него было в 3 раза дешевле, чем у остальных, а платить дороже мы не могли. Я решил рискнуть.

Когда она дала мне профиль Skype, там высветилось ”Vadim”, и после приветствия я поинтересовался, как так? Вадим сказал, что это фото его жены для привлечения внимания, а сейчас он даст контакт человека из Китая, который все сделает за $300, при этом тренировка языковой модели будет за его счет. За эти деньги никак нельзя было выполнить задачу, так как аренда серверов для этого тянула на несколько тысяч долларов. Но господин Ли заверил нас, что он будет работать в минус, чтобы набрать положительных отзывов и рейтинг. У него есть свои сервера. Много серверов! Охренеть! Через 2 дня он прислал натренированную языковую модель, которая давала отличное качество распознавания немецкого.

Здравствуйте, я ваша тетя!

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

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

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

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

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

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

— Привет, Алексей.

— Здравствуйте, а кто Вы?

— Я Ольга!

— Добрый день, Ольга. Что Вам нужно?

— Вас.

— А кто Вы?

— Я Ваша родственница.

У меня были родственницы с этим именем, но голос был абсолютно не похож.

— А по какому Вы вопросу?

— По важному!

— Ну так говорите!

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

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

Итоги

Когда я впервые смотрел сериал “Кремниевая долина”, он казался очень смешным и абсурдным. Наша фирма только что открылась, и нельзя было подумать, что в IT-бизнесе такое может происходить. Как же я ошибался! Оказалось, что такое количество приколов происходит у многих, и это становится забавным не сразу, а только через какое-то время. Ведь когда всё начинается, все на стрессе, и это воспринимается по-другому. Иногда кажется, что вместо разработки и продажи сервисов нужно было просто снимать сериал обо всем, что с нами происходило. Потом продать его Netflix и заработать миллиард. И взять Оскара, но это не точно.

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

Сейчас мы сфокусировались на 2-х продуктах:

a) неограниченном переводе на 100 языков,

b)неограниченной конвертации аудио в текст на 90 языков.

Почему это может быть кому-то интересно?

 Хотя бы потому, что за 200$ в месяц можно перевести 30 миллиардов символов и конвертировать 30000 часов аудио в текст. Это очень-очень много. При этом все ваши данные останутся у вас. Мы потратили несколько лет, чтобы достичь таких результатов.

За бесплатной демкой обращайтесь на info@lingvanex.com

 


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

Принцип «Трёх Амиго» в действии. Опыт с точки зрения тестировщика

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

Привет, уважаемые Хабровчане. Я — Алена Зорина, профдеформированный тестировщик и руководитель отдела обеспечения качества SSP. Предлагаю обсудить инструменты, которые помогают выстроить адекватную коммуникацию в команде, и при чем здесь тестировщики.


Когда в товарищах согласья нет

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

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

В итоге они договаривались, а потом шли и делали разные вещи. Аналитик в недоумении, потому что он же русским языком объяснил! Разработчик разводит руками: вот так же договорились.

К слову, процесс шел, и все было в общем‑то неплохо. Однако, при полном взаимопонимании получилось бы быстрее и приятнее для всех.

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

В этом и состоит принцип «Трех Амиго»: втроем договориться проще, быстрее и результат получается лучше.

Оптимальный состав

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

  • Если идет разработка нового продукта, то он через некоторое время дойдет до тестировщика. В любом случае придется погружаться;

  • На двоих аналитик и разработчик могут придумать и воплотить такие вещи, которые нельзя протестировать. А если их нельзя протестировать, то и делать не нужно;

  • Работа тестировщика — проверка на соответствие требованиям. То же единство глоссария, как в ситуации, которую я привела в начале, объект нашего внимания в любом проекте. Отсюда и берется третий и очень нужный контекст;

  • Достаточно спорная причина, но все же. Мое глубокое убеждение, что разработчики и аналитики мыслят в разных плоскостях. Аналитик настроен сделать по максимуму, предугадать все чаяния и надежды заказчика, максимально докрутить и усовершенствовать. Отсюда очень часто бывают лишние требования, фичи и задачи. Даже те, о которых клиент не подозревает и которые ему возможно вообще никогда не понадобятся.

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

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

Как мы выстроили этот процесс в SSP?

Сначала я прощупала почву и поняла, что коллеги в принципе не против видеть меня в качестве третьего.

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

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

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

Неожиданно про кадры

Следуя логике того, что тестировщики должны уметь коммуницировать с коллегами, я иначе посмотрела еще на один процесс: работу с персоналом.

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

Поэтому я решила работать над soft skills тестировщиков сразу с нескольких сторон. Во-первых, я замечаю ребят, которые хотят развивать навык коммуникации, и отправляю их на встречи на троих. Равномерно прокачиваю скиллы в отделе.

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

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

Итак, друзья, вывод из моих слов каждый сделает сам в меру своего опыта. Но я все-таки хочу подытожить. Мало начитаться умных статей о том, как теперь принято работать в прогрессивных компаниях. Нужно начинать с себя. Пробовать самому. И при любом удобном случае прокачивать soft skills.


ссылка на оригинал статьи https://habr.com/ru/company/ssp-soft/blog/725218/

@Transactional в Spring и исключения

Привет! В этой статье я бы хотел рассказать, как Spring’овая аннотация @Transactional ведет себя при возникновении исключений. Про это немало написано, в том числе на Хабре. Например, тут или тут. Поэтому, чтобы не повторяться, я не буду подробно расписывать как и почему исключения влияют на откат транзакций, а вместо этого просто покажу несколько примеров.

Если коротко, то Spring обрабатывает исключения в методах, помеченных @Transactional, следующим образом:

  • RuntimeException’ы приводят к откату транзакции, checked exception’ы не приводят;

  • RuntimeException’ы приводят к откату транзакции в момент, когда exception пересекает границы @Transactional-метода. Даже если вы перехватите это исключение выше по стеку, транзакция все равно откатится;

  • Этим поведением можно управлять через атрибуты rollbackFor / noRollbackFor у аннотации @Transcational.

Пример 1

@Slf4j @Component public class Bean {   private final BeanRepository repository;    public Bean (BeanRepository repository) {     this.repository = repository;   }    @Transactional   public void saveItems(List<Item> items) {     for(Item item: items) {       try {         saveItem(item);       } catch(RuntimeException e) {         log.error("Could not save item {}", item, e);       }     }   }    @Transactional   public void saveItem(Item item) {     if(item.foo().equals("BAD_ITEM"))        throw new RuntimeException();      repository.save(item);   } }

Что будет, если мы передадим для сохранения список Item‘ов, у которых 0-й и 2-й элемент НЕ являются BAD_ITEM’ами, а 1-й — является?

  1. Ничего не будет сохранено, поскольку RuntimeException в методе saveItem откатит транзакцию;

  2. Будет сохранен только 0-й элемент;

  3. Будет сохранен 0-й и 2-й элемент.

Ответ

Правильный ответ — 3 (ну, как правило, подробнее — ниже). Когда мы из компонента так вызываем метод того же компонента, это просто вызов внутреннего метода, а не метода прокси-объекта. Он попросту игнорирует аннотацию @Transactional у saveItem и не будет создавать новый транзакционный контекст. Почему так происходит можно прочитать в статьях, на которые я ссылался в начале. А значит RuntimeException не пересечет границу @Transactional-метода.

Если мы используем compile-time weaving, либо если мы перепишем код, чтобы вызов происходил через inner.saveItem, то правильным ответом будет 1.

Пример 2

@Component public class Bean {   private final BeanRepository repository;    public Bean(BeanRepository repository) {     this.repository = repository;   }    @Transactional   public void saveItem(Item item) {     if(item.foo().equals("BAD_ITEM"))        throw new RuntimeException();      repository.save(item);   } }  @Slf4j @Controller public class Controller {   private final Bean bean;    public Controller(Bean bean) {     this.bean = bean;   }      public void saveItems(List<Item> items) {     for(Item item: items) {       try {         bean.saveItem(item);       } catch(RuntimeException e) {         log.error("Could not save item {}", item, e);       }     }   } }

Тот же вопрос: что будет, если мы передадим для сохранения список Item‘ов, у которых 0-й и 2-й элемент НЕ являются BAD_ITEM’ами, а 1-й — является?

  1. Ничего не будет сохранено;

  2. Будет сохранен только 0-й элемент;

  3. Будет сохранен 0-й и 2-й элемент.

Ответ

Правильный ответ, по-прежнему, 3. Теперь транзакция с RuntimeException‘ом откатится, но этот код создает 3 транзакции. Остальные 2 будут закоммичены.

Пример 3

Все то же самое, но теперь метод saveItems тоже @Transactional:

@Slf4j @Controller public class Controller {   ...   @Transactional   public void saveItems(List<Item> items) {     for(Item item: items) {       try {         bean.saveItem(item);       } catch(RuntimeException e) {         log.error("Could not save item {}", item, e);       }     }   } }
  1. Ничего не будет сохранено;

  2. Будет сохранен только 0-й элемент;

  3. Будет сохранен 0-й и 2-й элемент.

Ответ

А вот теперь правильный ответ — 1. По умолчанию, @Transactional использует Propagation.REQUIRED, который приведет к тому, что saveItem будет использовать транзакцию, открытую для saveItems. При ошибке она будет помечена как rollbackOnly.

Пример 4

А теперь добавим в контроллер ограничение, что RuntimeException‘ы не должны откатываться.

@Slf4j @Controller public class Controller {   ...   @Transactional(noRollbackFor = RuntimeException.class)   public void saveItems(List<Item> items) {       try {         bean.saveItem(item);       } catch(RuntimeException e) {         log.error("Could not save item {}", item, e);       }   } }
  1. Ничего не будет сохранено;

  2. Будет сохранен только 0-й элемент;

  3. Будет сохранен 0-й и 2-й элемент.

Ответ

Правильный ответ все еще 1. noRollbackFor влияет только на аннотируемый метод, его поведение не «наследуется» компонентами ниже по стеку вызовов, даже если они используют ту же транзакцию. Поэтому saveItem пометит транзакцию как rollbackOnly.

Пример 5

Перенесем noRollbackFor на saveItem. А у контроллера уберем код по перехватыванию RuntimeException.

@Component public class Bean {   private final BeanRepository repository;    public Bean(BeanRepository repository) {     this.repository = repository;   }    @Transactional(noRollbackFor = RuntimeException.class)   public void saveItem(Item item) {     if(item.foo().equals("BAD_ITEM"))        throw new RuntimeException();      repository.save(item);   } }  @Slf4j @Controller public class Controller {   private final Bean bean;    public Controller(Bean bean) {     this.bean = bean;   }    @Transactional     public void saveItems(List<Item> items) {     for(Item item: items) {       bean.saveItem(item);     }   } }
  1. Ничего не будет сохранено

  2. Будет сохранен только 0-й элемент;

  3. Будет сохранен 0-й и 2-й элемент.

Ответ

Правильный ответ — 1. Да, теперь saveItem не откатывает транзакцию, но с самим-то RuntimeException‘ом он ничего не делает. Exception пролетит через контроллер, и уже у его @Transactional-метода вызовет откат транзакции.

Пример 6

Пометим оба @Transactional-метода так, чтобы они не откатывали runtime-исключения:

@Component public class Bean {   private final BeanRepository repository;    public Bean(BeanRepository repository) {     this.repository = repository;   }    @Transactional(noRollbackFor = RuntimeException.class)   public void saveItem(Item item) {     if(item.foo().equals("BAD_ITEM"))        throw new RuntimeException();      repository.save(item);   } }  @Slf4j @Controller public class Controller {   private final Bean bean;    public Controller(Bean bean) {     this.bean = bean;   }    @Transactional(noRollbackFor = RuntimeException.class)   public void saveItems(List<Item> items) {     for(Item item: items) {       bean.saveItem(item);     }   } }
  1. Ничего не будет сохранено

  2. Будет сохранен только 0-й элемент;

  3. Будет сохранен 0-й и 2-й элемент.

Ответ

Правильный ответ — 2. Транзакция не откатится, но из-за исключения обработка списка остановится. Тот же эффект будет, если вообще убрать аннотации @Transactional, либо если эту аннотацию убрать только у контроллера.

Пример 7

У Spring Data в интерфейсе CrudRepository есть такой вот метод:

void deleteAllById(Iterable<? extends ID> ids)

Что если ему на вход передать среди существующих идентификаторов один несуществующий? Я бы ожидал, что он удалит те, что есть, и проигнорирует те, которых нет (ну, как и обычный оператор delete в SQL). И да, Spring Data, именно так и сделает, но только если вы используете Spring Data 3. А ему на данный момент что-то около полугода. Если вы используете более раннюю версию, вы получите ошибку EmptyResultDataAccessException (Spring Data писали странные люди, и да, пошла она к черту эта ваша обратная совместимость).

@Component public class Bean {   private final BeanRepository repository;    public Bean(BeanRepository repository) {     this.repository = repository;   }    @Transactional(noRollbackFor = EmptyResultDataAccessException.class)   public void deleteItems(List<Item> items) {     repostory.deleteAllById(items.stream().map(Item::id).toList());   } }

Что будет, если в середине списка несуществующий айдишник в Spring Data 2.x?

Ответ

Транзакция полностью откатится. У Repository модифицирующие методы сами помечены как @Transactional, поэтому фактически этот пример эквивалентен примеру номер 4.

Вы можете в своем репозитории переопределить метод deleteAllById, указав уже ему noRollbackFor = EmptyResultDataAccessException.class, но это плохая идея. deleteAllById просто в цикле вызывает deleteById. Он тоже помечен как @Transactional, но это неважно, как мы выяснили в примере номер 1. Когда deleteById сгенерирует исключение, транзакция не откатится, но цикл прервется. В итоге, вы окажетесь в ситуации примера номер 6, когда половина Item‘ов удалилась, а половина — нет.

Выход — либо вместо deleteAllById проверять на стороне сервиса, что Item существует, а потом уже вызывать deleteById, либо удалять модифицирующим JPQL-запросом.

Пример 8

public interface BeanRepository extends JpaRepository<Item, Long> {    @Override   @Transactional(noRollbackForClassName = "org.springframework.dao.EmptyResultDataAccessException", rollbackFor = DataAccessException.class, noRollbackFor = RuntimeException.class)   void deleteAllById(Iterable<? extends Long> longs); }

Что будет если какого-то из айдишников нет?

Ответ

Ну, во-первых, автора такого пул-реквеста, скорее всего, побьют на code-review. Возможно, ногами.

Ответ — сработает то правило, которое содержит ближайшего родителя брошенного Exception‘а. В нашем случае — noRollbackForClassName, поэтому часть Item‘ов удалится, часть — нет.

Если родителя нет среди правил, будет использоваться дефолтное поведение (RuntimeException и Error откатываем, checked — не трогаем). Есть еще совсем упоротый вариант, когда написано что-то вроде rollbackFor = RuntimeException.class, noRollbackFor = RuntimeException.class. Spring разрешит сделать и это, к сожалению. Судя по исходному коду, всегда сработают noRollback-правила.

Итоги

  1. Атрибуты rollbackFor / noRollbackFor управляют только поведением транзакции в случае возникновения исключений. Сами исключения по-прежнему пробрасываются выше по стеку;

  2. Атрибуты rollbackFor / noRollbackFor не наследуются @Transactional-методами ниже по стеку вызовов, даже если используется Propagation.REQUIRED;

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


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

Zynq. AXI GPIO. Мигаем светодиодом по-новому

После написания последнего обзора на новую отладку Я не смог удержаться от того, чтобы не сделать простую проверку работоспособности платы, т.к. очень не хотелось бы напороться на какие-либо проблемы во время решения сложной задачи. Поэтому решил сделать простую мигалку светодиодами и задействовать, плюсом к этому, кнопки на плате. Немного поразмыслив, Я решил, что обычный “ногодрыг” на Verilog — это уже не так интересно и мне показалось, что лучше сделать это с помощью AXI GPIO и своего IP-ядра, инициировав экшн из baremetal-приложения. В общем, кому интересно, заглядывайте в статью, там Я описал, как добавить свое кастомное AXI Peripheral IP-ядро, как правильно организовать проект и обратиться к GPIO для чтения и записи логического уровня. Поехали…

Важно! Перед началом повествования, хотелось бы заранее оговориться, что основная цель, которую я преследую при написании этой статьи — рассказать о своем опыте, с чего можно начать, при изучении отладочных плат на базе Zynq. Я не являюсь профессиональным разработчиком под ПЛИС и SoC Zynq, не являюсь системным программистом под Linux и могу допускать какие-либо ошибки в использовании терминологии, использовать не самые оптимальные пути решения задач, etc. Но отмечу, что любая конструктивная и аргументированная критика только приветствуется. Что ж, поехали…

Как обычно, сначала создаем проект…

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

Далее выбираем RTL Project, оставляем включенной галочку Do not specify sources at this time и идем дальше. Находим интересующую нас модель SoC Zynq:

Тут стоит отдельно отметить, что у меня проект корректно работал на ZynqMini со 2-м Speed Grade. Невозможно было однозначно определить Speed Grade чипа, т.к. QR-код на чипе зашлифован. Надо будет проверить в более крупных проектах.

Идём далее и перед нами предстает окно Vivado, готовое к работе.

Постановка задачи

Итак. Прежде чем приступать к действиям, надо придумать интересную задачу и понять, что мы должны получить в результате. Предлагаю сделать следующее. Задействуем каждую из кнопок для включения разных сценариев моргания четырьмя светодиодами: первый сценарий — светодиоды будут загораться поочередно, второй сценарий — сменим направление у анимации, третий сценарий, когда нажаты обе кнопки — анимация сходится в центре, и по умолчанию просто мигаем светодиодами. Сделаем всё с использованием AXI GPIO, через свое собственное IP-ядро и запрограммируем логику работы кнопок через C-приложение которое мы запустим в baremetal на одном из ARM ядер. В целом, звучит не сложно. Идём дальше.

Создаем своё новое IP-ядро

В первую очередь создадим и опишем логику нашего собственного IP-ядра, который будет взаимодействовать с AXI-интерконнектом. Нажимаем в главном меню опцию Tools — Create and Package New IP

Откроется мастер создания IP-ядра, нажимаем Next и выбираем Create a new AXI4 peripheral:

Заполняем имя, версию и прочую информацию. Сразу советую использовать короткие имена и без дефисов в названии. У меня получилось вот так:

Описываем интерфейс AXI4 и его параметры:

После переключаемся в режим редактирования установив опцию Edit IP и нажимаем Finish:

Откроется отдельное окно в котором можно настроить дополнительные параметры IP-ядра:

Теперь создадим Verilog-файл, в котором мы опишем основную логику маршрутизации сигналов. Для этого в меню Sources нажимаем на синий крестик и вызовем мастер добавления Sources. Выберем пункт меню Add or create design sources.

В следующем меню нажимаем кнопку Create File и назовем его gpio_logic.v и выберем место хранения и нажимаем Finish:

В следующем окне, предлагающем нам определить порты I\O модуля — нажимаем OK,  мы это сделаем вручную. Откроем в списке Sources только что созданный файл и запишем в него следующее:

module gpio_logic(     // from buttons     input wire [1:0]gpio_input,     // to led pins     output wire [3:0]gpio_output,     // to zynq read     output wire [1:0]zynq_gpio_input,     // from zynq write     input wire [3:0]zynq_gpio_output );     assign zynq_gpio_input[1:0] = gpio_input[1:0];     assign gpio_output[3:0] = zynq_gpio_output[3:0];      endmodule

По сути, он связывает сигналы Zynq PS и PL. После этого открываем меню File Groups и открываем файл axi_gpio_button_and_led_v1_0.v, в него мы внесем некоторые изменения для корректной маршрутизации сигналов из AXI:

Открыв файл на редактирование находим блок в котором в комментариях написано Users to add ports here. Между комментариями мы запишем определение сигналов и сохраним изменения:

Запишем следующее (не забывая про запятые, т.к. это перечисление портов I\O): 

input wire [1:0] gpio_input,  // from FPGA pins output wire [3:0] gpio_output, // to FPGA pins

Затем изменим обработку сигналов на следующем уровне. Откроем файл axi_gpio_button_and_led_v1_0_S00_AXI.v. В него тоже запишем перечисление портов, как в предыдущем файле: 

Напишем следующее:

input wire [1:0] gpio_input,  // from FPGA pins output wire [3:0] gpio_output, // to FPGA pins

Сохраняем и переходим обратно к файлу axi_gpio_button_and_led_v1_0.v. Листаем ниже до пункта Instantiation of Axi Bus Interface S00_AXI. Там дополняем создаваемый экземпляр шины и дополним его нашими сигналами:

Пишем следующее:

.gpio_input(gpio_input), .gpio_output(gpio_output),

После этого в файле axi_gpio_button_and_led_v1_0_S00_AXI.v (строки 109110) закомментируем строки с объявлением регистров, которые мы не будем использовать и изменим register на wire в объявлении slv_reg1:

Теперь нужно закомментировать лишнее в строках 224, 225 и 226:

После закомментируем большой блок кода отвечающий за работу с неиспользуемыми регистрами:

Идем еще ниже и комментируем ещё три строки (263, 264, 265):

Спускаемся еще ниже и комментируем:

Итак, теперь можно считать, что наш сигнал регистров обработан и теперь можно связать экземпляр модуля gpio_logic.v с логикой AXI который мы создавали ранее.

Теперь мы заканчиваем с редактированием шаблона AXI-модуля и можно перейти к его упаковке. Открываем главное меню Package IP и нажимаем Merge changes from File Group Wizard:

После переходим в меню Customization Parameters и так же мерджим параметры:

Переходим в последний пункт и нажимаем Package IP:

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

Теперь можно конфигурировать Block Design и PS-часть Zynq.

Конфигурируем Zynq PS

Теперь когда наше IP-ядро готово, можем создать Block Design и слинковать всю логику. Нажимаем пункт меню Create Block Design и сразу можем добавить ZYNQ7 Processing System:

Переходим в настройки периферии. Глубоко конфигурировать тут ничего не придётся, нужно лишь включить UART1 на пинах 48 и 49:

И выбрать оперативную память MT41J256M16 RE-125 в 16-битном режиме:

Все остальное можно оставить по умолчанию. Добавляем на наш Block Design недавно созданное IP-ядро:

После можем запустить мастер Block Automation подсвеченный зеленым и выполнить все предложенные по умолчанию автоматизации. Теперь нужно сделать порты gpio_input и gpio_output в нашем IP-ядре внешними, нажав на них правой кнопкой и выполнив команду Make External. Получится следующая картина:

Проверим, что адресное пространство AXI-блока с которым будем взаимодействовать начинается с адреса 0x43C00000:

После можно создавать HDL Wrapper, нажав правой клавишей мыши по созданному нами Block Design:

Оставляем опцию по умолчанию:

После так же развернем Block Design иерархию и сделаем Generate Output Products и нажимаем Generate:

Потребуется некоторое время на генерацию. И после запустится синтез полученных исходников. После окончания синтеза нажимаем Open Synthesized Design:

После того как закончится эта операция — нужно перейти к разметке пинов GPIO. Но перед этим необходимо сохранить сделанные изменения и сгенерированный Constraints File. Нажимаем в меню Open Elaborated Design и нажимаем Ctrl + S:

Записываем имя файла, сохраняем и можем запустить синтез, чтобы потом сделать назначение I\O интересующих нас пинов. После окончания открываем синтезированный дизайн и у нас откроется меню Package и I/O Ports:

Все наши GPIO пины используются в логике LVCMOS33, поэтому выставим им это значение и заполним используемые пины в соответствии со схематиком присланным производителем. После внесения информации о пинах — сохраняем и запускаем генерацию Bitstream. 

Дожидаемся окончания генерации:

Экспортируем Hardware-файлы, для последующего их использования в SDK:

Ставим галочку в пункте Include bitstream и нажимаем ОК:

После запускаем меню File — Launch SDK и переходим к созданию baremetal-приложения.

Создаем приложение и моргнем светодиодами

Создаем новое приложение в SDK через меню File — New — Application Project:

Пишем имя проекта и нажимаем Next и выбираем Hello World:

В дереве проекта находим файл helloworld.c и в нем опишем логику соответствующую нашему замыслу:

В этот файл вносим изменения и описываем логику:

#include <stdio.h> #include "platform.h" #include "xil_printf.h" #include "sleep.h" #include "string.h"  #define IIC_BASEADDRESS 0x43C00000 #define REG0_OFFSET 0 #define REG1_OFFSET 4  u32 gpio_input_value = 0; char buf_print[64] = {0};  int main() {     int i = 0;     init_platform();      while(1)     { for(i=0; i<64; i++) buf_print[i] = 0;     gpio_input_value = Xil_In32(IIC_BASEADDRESS + REG1_OFFSET);     sprintf(buf_print, "input gpio_value = %d\r\n", gpio_input_value);     print(buf_print);      gpio_input_value = Xil_In32(IIC_BASEADDRESS + REG1_OFFSET);      if (gpio_input_value == 2)     {     Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x0);     usleep(100000);      Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x1);     usleep(100000);      Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x2);     usleep(100000);      Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x4);     usleep(100000);      Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x8);     usleep(100000);      }     else if (gpio_input_value == 1)     {     Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x8);     usleep(100000);      Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x4);     usleep(100000);      Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x2);     usleep(100000);      Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x1);     usleep(100000);      Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x0);     usleep(100000);     }     else if (gpio_input_value == 0)     {     Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x9);         usleep(100000);          Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x6);         usleep(100000);          Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x0);         usleep(100000);          Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x6);         usleep(100000);          Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x9);         usleep(100000);     }     else if(gpio_input_value == 3)     {     Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0xF);     usleep(100000);      Xil_Out32(IIC_BASEADDRESS + REG0_OFFSET, 0x0);     usleep(100000);     }     }      cleanup_platform();     return 0; }

После этого можно перейти в меню Xilinx — Program FPGA и запускаем bitstream-файл. При успешном запуске будет включен светодиод PL DONE. После этого кликаем правой кнопкой в дереве проектов на имени проекта и в контекстном меню выбираем Run As — Launch On Hardware (System Hardware).

После запуска — светодиоды будут мигать и от нажатия клавиш будут изменяться анимации светодиодов. Плюсом к этому если подключить USB-кабель в порт UART — можно увидеть текущее значение регистра в который записывается состояние входных сигналов. Если ни одна из кнопок не нажата — будет возвращено значение 0x3, если одна кнопка нажата — будет возвращаться знание 0x2 или 0x1, в зависимости от кнопки и 0x0 если зажаты две кнопки одновременно.

Будем считать, что цель достигнута. То есть мы через взаимодействие с AXI прямой записью\чтением по адресу памяти поработали с GPIO. А теперь в следующей главе разберем все грабли которые Я собрал, пока решал эту задачу. 

Танцы на граблях

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

Одна из проблем, связана с длинной имени IP-ядра. Длинные имена не очень подходят для IP-ядер, плюсом использование знаков “дефис” — тоже видимо противоречит правилам именования в Vivado. Кажется, пробежки по таким граблям неизбежны.

Вторая проблема которая возникла состоит в непонятной невозможности перенести изменения в кастомном IP-блоке в проект. Идея была в следующем. Сначала Я сделал входной сигнал с одной кнопки, чтобы проверить, что все работает, прежде чем подключать вторую. Проверил — работает. После внес изменения в IP-ядро, везде все обновил — и ни в какую не получилось получить шину вместо wire в результатах синтеза. Открыл в меню RTL ANALYSIS — Open Elaborated Design и начал просматривать схематик сигналов, чтобы понять, где у меня проблема. И обнаружил, что даже после внесения изменений не изменяется количество сигнальных проводников: 

И что бы я ни делал, как бы не изменял IP-ядро — не получилось добиться того, чтобы был шинный интерфейс [1:0] вместо одного проводника gpio_input. Полное пересоздание проекта и IP-ядра помогло решить эту проблему:

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

Исправив эту проблему, я столкнулся со следующей. Я не понял, почему при запуске проекта в SDK — не загружается автоматом сгенерированный bitstream-файл в FPGA. Поковырявшись в настройках Debug-конфигурации — нашел, где включается этот параметр: в структуре проектов кликаем правой кнопкой, открываем меню Properties и переходим в самый нижний пункт Run/Debug Settings и нажимаем Edit на первом варианте конфигурации:

Устанавливаем галочки у пунктов обозначенных стрелками:

После запуска через меню Run — у нас сначала прошьется FPGA, а потом будет запущено приложение.

Заключение

По итогу этого урока, мы поморгали светодиодами, обработали сигналы с кнопок с использованием кастомного AXI IP-блока. По ходу подготовки материала для статьи пришлось немного подебажить проект. Я постарался максимально полно описать грабли, по которым пришлось пробежаться т.к. на сегодняшний день Я считаю, что в этом состоит основная ценность материалов, подобных этой статье и имеет гораздо больший вес, нежели тупое описание step-by-step.

P.S. В следующей статье, попробуем сделать более интересную задачу и выведем картинку на OLED-дисплей, который подключен к PL-части.


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

Какую Data Science школу выбрать?

Сегодня мы рассмотрим три школы: ШАД от Яндекс, Школу анализа данных от VK и AI Masters.

Школа анализа данных ШАД, Школа анализа данных от Vk и AI Masters — это образовательные учреждения, специализирующиеся на обучении анализу данных и машинному обучению. Они имеют некоторые сходства, но также и различия, которые могут быть важны для потенциальных студентов при выборе школы.

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

ШАД
Школа Анализа Данных (ШАД) — образовательная программа, созданная Яндексом, одной из крупнейших IT-компаний России. ШАД имеет высокий авторитет в России и за ее пределами. Если вы хотите получить мощную программу PhD в США или работать на позиции Data Scientist в крупных IT-компаниях, то ШАД — отличный выбор. 

 VK
Школа анализа данных от VK — это программа для специалистов, уже имеющих опыт работы от 1 до 3 лет в сфере анализа данных или разработки. Она подходит для тех, кто знает языки разработки, такие как Java и Python.

 AI Masters
AI Masters — это образовательная программа в области наук о данных от Института ИИ МГУ. Курсы соответствуют магистерским программам в области машинного обучения и бизнес-аналитики от ведущих университетов мира.

ПОСТУПЛЕНИЕ

ШАД
ШАД Яндекса всегда славился своим жестким отбором. 
Претендентам необходимо пройти три этапа: онлайн-тестирование, онлайн-экзамен и личное интервью. 

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

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

AI Masters
Отбор в AI Masters начинается с прохождения онлайн-тестов по стандартным математическим дисциплинам. Затем необходимо пройти контест по BI и DS или сдать онлайн-экзамен. 

ПРОГРАММЫ

ШАД
ШАД Яндекс предоставляет 5 программ на выбор.

⁃ Data Science для тех, кто хочет научиться работать с данными и машинному обучению.

⁃ Разработка машинного обучения для тех, кто хочет и кодить и понимать Data Science.

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

⁃ Анализ данных в прикладных науках для тех, кто хочет применять Data Science в областях, не связанных с IT

⁃ Альтернативный трек поступления для тех, кто имеет опыт промышленной разработки и Data Science.

VK
Школа анализа данных от VK предоставляет 3 программы на выбор.

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

⁃ Machine learning для тех, кто хочет решать задачи с помощью машинного обучения и внедрять свои решения в эксплуатацию.

⁃  Machine learning operations engineer для тех, кто хочет поддерживать и развивать инфраструктуру обучения моделей. 

AI Masters
AI Masters предлагают 2 направления.

— Data Science and Data Engineering для тех, кто хочет жёстко ботать математику и много практиковаться в области анализа данных.

— Business Intelligence для тех, кто мечтает стать бизнес-аналитиком с глубоким знанием анализа данных.

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

Как проходит учеба в школах?

Обучение по всем программам проходит на бесплатной основе.

ШАД Яндекс — занятия по вечерам 3 раза в неделю, много домашних заданий, 3 курса за семестр, всего обучение длится 4 семестра. Учиться надо много по 30-40 часов в неделю. Можно полностью удаленно учиться и общаться со студентами и преподавателями из ШАД. 

В Школе анализа данных от VK обучение длится 2 семестра. Учиться надо по 25-40 часов в неделю. Можно очно учиться в МИСИС или удаленно. Нет платного отделения.

В AI Masters обучение длится 2 года по вечерам, занятость примерно 30 часов в неделю.

 ВЫВОДЫ

Таким образом, ШАД Яндекс предназначен для будущих экспертов, Школа анализа данных от VK — для людей с опытом, а AI Masters — для тех, кто стремится стать бизнес-аналитиком или математическим гуру. 

Для поступления в эти школы необходимо иметь твердые знания в математике, опыт участия в олимпиадах по программированию или индустриальный опыт в области Data Science. Однако, главное — это готовность учиться много и постоянно, независимо от выбранного пути.


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