Русский
Русский
English
Статистика
Реклама

Алгоритмы

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

10.06.2021 12:09:47 | Автор: admin
Аристотель придумал, что в риторике есть четыре главных способа убеждения. Пафос через эмоции и повествование. Логос через факты и логику. Этос через авторитет того, что убеждает. Есть еще кайрос, фактор контекста: того, когда вас пытаются убедить и при каких условиях.

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

Что же, ответ у меня есть. Я считаю, что этос это бич мыслящего человека. Кейрос кайданы, который выковал информационный век. А понимание того, как работает риторика базовый инструмент критического мышления. Особенно для IT-специалиста.

Я так много рассказывал об этом на кухнях и в чатах, что решил написать статью. А получился лонгрид с научными исследованиями, разбором влияния алгоритмических новостных лент, и безумным комиксом из мемов, который я делал 4 часа в Фигме. Поехали!


Доверяй да проверяй: проблема экспертов


В любой сложной теме, СМИ и блогеры в первую очередь обращаются к экспертам. Врача слушать о медицине надежнее, чем копирайтера за 50 рублей/килознак. Отставной военный разбирается в войне в Сирии лучше, чем банковский клерк Славик. Трейдер лучше объяснит о будущем криптовалют, чем скептик из улицы. Ну а то, что есть на завтрак Герман Греф, интереснее для публики содержимого вашего собственного завтрака. Это всё этос, убеждающая и привлекающая способность авторитета и знаменитости.

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

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

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

А вот в прогнозировании будущего, эксперты справляются хуже. И это не шутки: исследования Филиппа Тетлока показали, что среднестатистический эксперт-политолог справляется хуже, чем статистический анализ[2]. И что специалисты, что статистика справились хуже, чем шимпанзе, мечущий дротики в мишень. А в случае с предсказаниями котировок фондового рынка, усредненный диванный эксперт из интернета справляется лучше, чем профессиональный аналитик из банка[3][4]. Об этом Нассим Талеб рассказывает половину своей книги Черный Лебедь, повторяя на всякий лад.

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

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

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

Этос: мастер-класс на пальцах


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

Как состоявшийся спорщик но начинающий писатель, я влияние этоса испытал на себе сполна. Когда у меня в Психотронке 30 подписчиков не считая собаки, а у блогера-политолога их 300 тысяч, его этоса хватит, чтобы при встрече в чате, толпа стала на его сторону вне зависимости от темы спора. В моем случае, блогер доказывал, что совершенно невозможно получить Ph.D. в США, цитируя Библию как источник в научной работе. Тем более, в медицинской специальности. Ведь в цивилизованном мире так дела не делаются.

Я не осуждаю людей. Логичнее доверять экспертному блогеру, ведь человеку есть что сказать. Однако исследования показывают, что с ростом авторитетности и известности эксперта, растет только его уверенность в собственных предсказаниях, но никак не точность этих предсказаний[2]. В той истории с Библией, к блогеру прилетел черный лебедь, ведь соискатель Ph.D. с Библией был моим клиентом. И моим клиентом он стал потому, что его куратор цитирование Библии требовал. Но меня все равно признали фантазером и забанили.

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

Но подвох в том, что до марта 2020 года, Томас писал только о инфобизнесе (которым сейчас и занимается), личном успехе и позитивном мышлении. И его лучшие статьи назывались "Как написать смешную речь?" или "Как умерить ожидания CEO". Но с марта 2020-го, естественно, Томас писал только о коронавирусе, ведь по его же словам 20, он получил в тысячи раз больше просмотров, чем обычно.

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

Итак, вопрос. Хотели ли бы вы, чтобы Томас был министром здравоохранения в вашей стране и принимал решения по борьбе с эпидемией? Последние три абзаца, в сочетании с картинкой выше, заставляли бета-тестеров этого текста говорить нет. Именно так работает риторика, ведь этос Томаса к этому моменту серьезно пострадал.

Правильный ответ другой. По крайней мере, британцы бы предпочли Томаса своим министрам. В апреле 2020, Томас в гостях у MIT Technology Review упоминает о том, что Британия поздно последовала его совету превентивных карантинов, и поплатится за это жизнями[20]. В январе 2021, Борис Джонсон, премьер-министр Британии, будет просить прощения за 100,000 смертей от коронавируса в том числе, из-за промедления с карантином.

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

В итоге, чтобы есть досыта и не стать едой самому, приходится думать самостоятельно. И думает каждый по своему карману. Мыслители при деньгах, вроде основателя Амазона Безоса, нанимают штат разноплановых экспертов. Их заставляют работать в команде и пользоваться проверенными методами; это увеличивает точность предсказаний[2]. В итоге, Безос хвастается тем, что его работа думать о проблемах на 2-3 года в будущем, а сегодняшние беды разребают мальчики на побегушках[5]. Судя по всему, пока его подход работает.

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

Прирученный алгоритмами кайрос


Кайрос то есть влияние контекста и своевременности подачи на убедительность информации тоже известен со времен греков[6], но менее известен, чем его три брата. Настолько менее известен, что на русскоязычной вики о нем нет статьи, а в риторику его включают только самые хорошие (читай: самые дорогие) колледжи. Так случилось потому, что кайрос до ХХ века был вещью очень очевидной. В летнюю жару предложение искупаться соблазнительнее, чем во вьюгу. Бедный охотнее выступит за налоги для богатых, чем богатый. В общем, банальности.

Разумеется, манипулировать контекстом додумались тоже давно. Революции происходили тогда, когда было много голодных и недовольных. Газеты почти с самого своего появления занялись рекламой, а потом борьбой за то, чтобы быть главным источником новостей[7]. А зачем это им было нужно, догадаться несложно.

Даже сайты до появления адблока продолжали многовековые традиции.

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

Но в ХХ веке случилась перемена. Первое что большую часть своей познавательной деятельности человек стал проводить в интернете. Второе что информационную среду человека перестал определять его выбор, будь то выбор газеты, бара или места жительства. На сцену вышли контентные алгоритмы, со временем потеснив прежнего лидера распространения информации соседей[8]. То, что показывает Фейсбук в ленте, Ютуб и Яндекс Новости, формируется специально исходя из предпочтений пользователя и его поведения. Эдакой личный тайный советник для каждого.

Как человек, который постоянно работает с рекламой, я уверяю вас (поняли подвох, да?), что считаются не только лайки. Длительность чтения, внимательность, соответствие переписке, которую тоже анализируют работы всё это учитывается для того, чтобы показать вам правильный набор постов. На Хабре есть профессионалы по таргетингу рекламы и те, кто делает контентные алгоритмы, так что микрофон тут передаю им. Вот тут можно почитать, например, сколько всего Фейсбук знает о пользователе, а тут небольшой обзор на алгоритмы Ютуба.

При этом, цель контентных алгоритмов не в том, чтобы пользователь стал умнее, сильнее, или хотя бы счастливее. Их цель увеличить время пользователей за просмотром контента, а вместе с этим количество взаимодействий с рекламой. Взаимодействия важны: пассивные зрители менее ценны чем те, который лайкают (или дизлайкают) и комментируют. А лучше всего, когда в контент вовлечены другие и друзья: тогда зависнуть можно надолго[9].

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

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

Самоисполняющиеся пророчества


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

Контент с риторикой логоса, вроде Итоги личного опыта пользования Linux на разныхы платформах, вызывает меньше реакции чем риторика пафоса: 10 причин ненавидеть Linux: исповедь бородатого юзера [10]. Это мотивирует производителей контента сменять разумные доводы эмоциональными ради больших охватов. Второй факт: адаптация контента под конкретную аудиторию и тему приводит к большему росту популярности, чем нейтральность, объективность и широкий обзор.

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

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

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

Истории получаются едва не библейские. Например, книжный блогер 5 лет обозревает хорошую литературу и не получает ни гроша. А знаменитостью становится, как только начинает обозревать треш для подростков[11]. Без слез смотреть нельзя.

Ранее, контентные алгоритмы замечались в еще более грязной игре. Например, женщинам показывались их более успешные знакомые, потому что наблюдение за ними затягивает. Но психиатры обнаружили вредный эффект такого социального сравнения и забили тревогу[12], и Фейсбук лавочку прикрыл. Из-за похожего давления, Фейсбук в 2018 году перекрутил алгоритмы, уведя трафик рекомендаций от брендовых страниц к страницам реальных людей[13]. История, где Фейсбук вдруг осознает, как плохо он поступал, и перекраивает все алгоритмы, продолжается до сих пор.

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

Война воображаемых миров


Главная беда алгоритмических лент в том, что они приводят к сильному сужению разнообразия точек зрений, доступных человеку. В свою очередь, у нас есть встроенное когнитивное искажение, эвристика доступности, из-за которой события кажутся более вероятными, правдивыми или частыми, если легче вспоминаются. Соединение алгоритмической ленты и эвристики доступности создает ситуацию, где человек постепенно убеждается, что его картина мира является единственно возможной. Этот феномен стал известен как информационный пузырь, по-английски Filter Bubble[14].

Ученые считают алгоритмические пузыри причиной поляризации политических и социальных движений в последнее время например, в США[15]. Те же ненавидимые всеми в рунете SJW просто варятся Твиттером, Фейсбуком и Ютубом в сообществе согласных между собой людей. Оно существует по четким для них правилам, в нем происходят события по определенным сообществом причинам; это де-факто отдельный мир со своими физическими законами. И когда большой инфо-повод или ошибка алгоритма вкидывает к ним события из другого пузыря например, объективированных женщин с громадной грудью из пузыря геймеров SJW испытывают культурный шок.

Та самая политическая поляризация и радикализация в США. Источник с большими размерами и анимацией

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

И это работает во все стороны. Мы на Хабре непонятные пришельцы для мира открыток из Одноклассников. Когда Джавараш переводили свои курсы для англоязычной аудитории, англоязычные возмущались, что робот удивляется цветным волосам женщины[16]. А теперь и дня не проходит без вскрытия очередного информационного пузыря.Журнал Рабдно тому пример: если хотите увидеть, как выглядит культурный шок, покажите тимлиду из Москвы интервью с воспитателем детского сада, который питается объедками, или моряком, который неделями может ходить на горящем корабле.

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

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

В итоге, кайрос препятствует тем, кто прошел первый уровень квеста к критическому мышлению и поборол власть этоса. Когда точки зрения выдаются строго под момент, а их разнообразие дозировано, чтобы вы не удалились из Фейсбука, нельзя прийти к нестандартному выводу. Когда в Гугле сайты, на которых стоит больше всего обратных ссылок (читай: вложено больше денег), показываются выше других[18], нестандартные выводы даже не нагуглить.

И это если вам повезет понять, что ваша картина мира не соответствует действительности.




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

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

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

И главное: не забывайте о рациональности. Новости и информационные пузыри довели уже до того, что американские бедняки голосуют за Республиканцев, в большинстве своем крупных и наследственных богачей[19]. Действуйте в первую очередь в интересах своих и своей семьи. И только потом в интересах государства, народа, и прочих воображаемых персонажей.

Источники и комментарии к ним
Если вы впервые решили почитать научную литературу, вас могут порадовать цены от $35. Деньги, однако, достаются не ученым, а издателям. Совет от бывалых: ученые всего мира *покупают* статьи на SciHub и LibGen.

Ищите по DOI (обычно можно найти в URL или возле заголовка), или по URL.

[1]: Chudek, M et al. (2013). Culture-gene coevolution, large-scale cooperation, and the shaping of human social psychology. In K. Sterelny, R. Joyce, B. Calcott, & B. Fraser (Eds.), Life and mind: Philosophical issues in biology and psychology. Cooperation and its evolution (p. 425457).

[2]:What Research Tells Us About Making Accurate Predictions,* Harvard Business Review, 2015. Это краткий пересказ исследований Ф. Тетлока, оригинальная книга называется Expert Political Judgment.

[3]: Nofer M. (2015) Are Crowds on the Internet Wiser than Experts? In: The Value of Social Media for Predicting Stock Returns.

[4]:Lang et al. (2016). How Crowdsourcing Improves Prediction of Market-Oriented Outcomes. Journal of Business Research, 69:10, p. 4168-4176

[5]: VC.ru (2020).Джефф Безос рассказал о своих принципах принятия важных бизнес-решений. Из Invent and Wander: The Collected Writings of Jeff Bezos.

[6]: Kairos.Wikipedia.

[7]: Musson, A. (1958).Newspaper Printing in the Industrial Revolution. The Economic History Review, 10(3), 411.

[8]: Dietz, R. (2002).The estimation of neighborhood effects in the social sciences: An interdisciplinary approach. Social Science Research, 31(4), 539-575. Хорошая обзорная статья на тему того, что раньше думали социологи о соседях. Сейчас всё пишут об интернете и наркотиках, скукота.

[9]: DeVito, M. (2016).From Editors to Algorithms. Digital Journalism, 5(6), 753-773. Автор пытается деконструировать алгоритмы новостей Фейсбука по общедоступной информации. На мой вкус, с тех пор изменилось не так много.

[10]: Lee D. at al. (2014). The Effect of Social Media Marketing Content on Consumer Engagement: Evidence from Facebook. ISIS Conference. Кстати, интересный кейс использования Amazon Mechanical Turk для обработки большого массива комментов и сообщений.


[11]: Книжный чел. (2021). Энтони Юлай: плохие книги, будни книжного блогера. #74 на YouTube.


[12]: de Vries, D., & Khne, R. (2015). Facebook and self-perception: Individual susceptibility to negative social comparison on Facebook. Personality And Individual Differences, 86, 217-221.


[13]: Pleshakov D. (2018). Facebook feed update: what happened and what to do? Captain Growth Blog.


[14]: Filter Bubble. Wikipedia.


[15]: Molla R. (2020). Social media is making a bad political situation worse. Vox.com


[16]: Еленевич А. (2020). Как JavaRush запускали продукт в США: ошибки, которые не стоит повторять. VC.ru


[17]: Ostracism. Wikipedia.


[18]: Semrush. (2020). Ranking Factors Study 2.0. Интереснейшее чтиво о том, что на самом деле влияет на поисковую выдачу Гугла.


[19]: Barber, N., Ph.D, (2020, July 31). Why Do Many Poor People Vote Republican? Psychology Today.


[20]: MIT Technology Review (2020, April 3). How to make sense of all the information about coronavirus.






Подробнее..

Исследование операций

16.06.2021 16:13:28 | Автор: admin
Cодержание
  1. Введение

  2. Основные понятия и термины

  3. Характеристика ИО как научной дисциплины

  4. Этапы операционного исследования

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

    • Построение математической модели

    • Нахождение решения с помощью математической модели

    • Проверка модели и решения

    • Построение процедуры подстройки и решения

    • Осуществление решения

  5. Предметные процессы ИО и задачи

    • Процессы обслуживания

    • Распределения

    • Управления запасами

    • Замены

    • Состязательные

  6. Модели процессов ИО и их логическая структура

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

    • Элементы задачи ИО

    • Взаимодействие исполнения и управления при ИО

  7. Литература

Введение

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

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

Владелец транспортных средств располагает матрицей:

С = [c_{ij}], i = 1(1)n, j = 1(1)n,

стоимости (затрат) топлива в операции перевозки.

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

Q(X[i,j]) = \sum_{i=1}^{n}\sum_{j=1}^{n}{c_{ij}x_{ij}} min.

Это соотношение называют целевой функцией (ЦФ). Неизвестными здесь являются переменные (xij) , т.е. куда тягач с номером (i) должен доставить груженый прицеп с номером (j). Ясно, что суммарные затраты будут определяться планом Х[i, j] перевозок или выбором переменных (xij) в совокупности. Постоянно будем иметь ввиду очевидное ограничивающее условие: один тягач везет один прицеп, каждый прицеп обслуживается одним тягачом. Из этого условия следует, что каждый элемент (xij), выбираемый для подстановки в ЦФ, не может быть дробным значением, а принимает одно из значений 0 или 1, т.е. (xij 0, i = 1(1)n, j = 1(1)n).Приведенное выше условие интерпретируется в модели как ограничения, накладываемые напеременные (xij).

(i = 1) х1112 ++ х1j + + x1n = 1, j = 1(1)n;

(i = 2) х2122 ++ х2j + + x2n = 1, j = 1(1)n;

(i = 3) х3132 ++ х3j + + x3n = 1, j = 1(1)n;

.. .

(i = i) хi1 + хi2 + + хij + + x in = 1, j = 1(1)n;

. .. .. . . .. .

(i = n) хn1n2 ++ хnj + + x nn = 1; j = 1(1)n.

Каждая (i-я) строка обозначает возможность прибытия на (j-й) склад любого (j-го) из (n) тягачей и представляется суммой, в которой будет выбран один единственный элемент (xij = 1). Относительно столбцов j = 1(1)n системы уравнений также составляются суммы, и каждая из таких сумм также равна единице. В столбцовых суммах также выбирается единственный элемент(xij = 1) Таким образом, будет выбрана таблица (0,1-матрица), заполненная единицами и нулями, но так, что в каждой строке и в каждом столбце оказывается единственная единица. Такие матрицы в математической статистике называются дважды стохастическими. Каждая матрица реализует план Х перевозок и ему соответствует определенное (после подстановки переменных плана в выражение целевой функции) значение ЦФ. Сколько же планов-решений Х можно построить? Это легко посчитать. Первый тягач можно направить в любой из n складов, но второму тягачу будут доступны только n1 складов, третьему n2 складов. Этим трем тягачам соответствует n(n1)(n2) количество планов равное произведению трех сомножителей. Далее число возможностей выбора склада будет сокращаться (для 4-го тягача только n3 выборов), а для последнего nго тягача останется единственная возможность, так как все остальные склады уже распределены между n1 тягачами, всего планов будет |Х[i, j]|= n! Например, если n = 20, то

|Х[i, j]| = 20! = 2 432 902 008 176 640000.

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

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

Основные понятия и термины ИО

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

Цель это описание желаемого состояния или запланированного результата деятельности, на создание (получение) которого направлены ресурсы, усилия. Чаще всего цель или цели (дерево целей) оформляются в форме требуемых значений различных показателей (в ТТЗ на НИР, ОКР и др.)

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

Решение это какой-то конкретный выбор из полного ряда возможностей (ограничения задачи не учитываются).

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

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

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

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

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

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

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

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

Характеристика научной дисциплины ИО

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

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

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

Предметные процессы ИО и задачи

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

Процессы обслуживания

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

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

Процессы распределения

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

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

Процессы управления запасами

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

Процессы замены

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

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

Состязательные процессы

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

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

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

Этапы операционного исследования

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

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

  2. Построение математической модели изучаемой системы

  3. Нахождение решения с помощью модели.

  4. Проверка модели и получение с ее помощью решения.

  5. Построение процедуры подстройки решения.

  6. Осуществление решения.

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

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

Е = Аrgextr F(x,y, ui),

где Е показатель эффективности системы; х,у неуправляемые переменные, ui управляемые переменные, F(x,y, ui) - целевая функция модели. Ограничения, накладываемые на переменные модели, выражаются системами равенств или неравенств дополнительно к основному соотношению показателю эффективности модели, Аrgextr F(x,y, ui) критерий эффективности примененный к целевой функции. Следует заметить, что в публикациях авторы критерием эффективности называют показатель эффективности, а критерий вообще опускается из рассмотрения. КЭ, трактуемый как правило (руководство), не измеряется числом. К этим вопросам следует отнести и вопрос о существовании разных шкал для проведения измерений с учетом отношения, отвечающего той или иной шкале.

Таблица шкал измерений переменных и отношения (МО - метризованное отношение)Таблица шкал измерений переменных и отношения (МО - метризованное отношение)

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

Рисунок 1 Схема постановки задачи и получения логико-структурных решенийРисунок 1 Схема постановки задачи и получения логико-структурных решений

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

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

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

Методы реализации процессов ИО и их логическая структура

Рене Декарт, описывая развитый им метод научного исследования, формулирует четыре основных его правила [3].

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

  2. Нерешенные проблемы следует сводить к решенным. Этим путем находятся решения простых проблем.

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

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

Следует сказать несколько слов о логике исследований и методологических понятиях [2}. В методологии науки разработаны общие понятия для описания конкретных методов и проводимых в их рамках процессов научного исследования. Эти описания играют важную роль при выявлении связей между различными методами в составе сложных исследований. Поэтому необходимо использовать эти понятия для методологического описания логики проводимых исследований.

Так проверка и корректировка гипотез производятся с помощью хорошо известных пяти методов индукции, создание и развитие которых связано с именами Ф. Бэкона и Дж. Милля [4, 5].

Рисунок 2 Логическая структура проверки гипотез и процесса логико-структурных решенийРисунок 2 Логическая структура проверки гипотез и процесса логико-структурных решений

Причинная связь явлений устанавливается методами единственного сходства (на основе фактов, полученных в наблюдении), единственного различия ( на основе фактов, полученных в эксперименте), объединенным методом (на основе совместно используемых фактов обоих видов). Метод остатков применяется при выявлении неизвестной причины изучаемого явления, а с помощью метода сопутствующих изменений анализируется динамика причинных зависимостей [6, 7 , 9].

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

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

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

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

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

Элементы задачи ИО

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

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

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

В-четвертых, необходима (должна существовать) возможность выбора между различными (по крайней мере двумя) способами действия или политиками. При отсутствии выбора задача не возникает. Задача всегда включает вопрос: что делать? Но сам этот вопрос становится задачей только в том случае, если существуют различные пути достижения цели. Различные способы действия (стратегии) являются четвертым элементом задачи.

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

  1. Уточнить перечень целей, определенных на первом этапе постановки задачи.

  2. Уточнить перечень различных стратегий достижения целей.

  3. Определить меру (показатель и критерий) эффективности исследуемой системы.

Взаимодействие исполнения и управления при ИО

Планирующая система связывает объект управления с управляющими органами. В процессе деятельности объекта (рис. 3) происходит преобразование одного состояния объекта в другое (10), что зависит от изменения внешних обстоятельств (9) и предписаний (8), вырабатываемых органами управления на основании плановых представлений объекта (5) и оценок текущего состояния объекта управления (7). Оценки вырабатываются на основании показателей и критериев эффективности (5) и информации (6) от объекта управления. Вся эта цепочка определяет один такт в преобразовании объекта из одного состояния в другое. Для каждого такта необходима своя часть плана. Поэтому после выполнения одного такта в самом плане управление передается следующей его части, что обозначается стрелкой (2) на рис. 3.

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

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

Рисунок 3 План и его связи с частями объектаРисунок 3 План и его связи с частями объекта

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

Литература

1.Азгальдов Г. С., Райхман Э. П. О квалиметрии. М.: Изд. Стандартов, 1973.172 с.

2.Ваулин А. Е. Методы цифровой обработки данных. СПб.: ВИККИ им. А. Ф. Можайского, 1993. 106 с.

3. Декарт Р. Рассуждения о методе с приложениями (Диоптрика, Метеоры, Геометрия) М.: Изд-во АН СССР, 1953.с.9-66.

4.Бэкон Ф. Новый органон. М.: Соцэкгиз,1938. 244 с.

5.Гэри М., Джонсон Д. Вычислительные машины и трудно решаемые задачи. М.: Мир, 1982.

6. Джини К. Логика в статистике. М.: Статистика,1973. 127 с.

7. Квейд Э. Анализ сложных систем. М.: Советское радио,1969.519 с.

8.Квейд Э. Методы системного анализа // Новое в теории и практике управления производством в США.М.: Прогресс, 1971. с.78-99.

9. Корбут А.А., Финкельштейн Ю. Ю. Дискретное программирование М. Наука. Гл. ред. физ.-мат. лит. 1969.

10.Клыков Ю. И. Ситуационное управление большими системами. М.: Энергия,1974.135 с.

11. Макаров И. М. и др. Теория выбора и принятия решений. М.: Наука, 1982. 328 с.

12.ПфанцагльИ. Теория измерений. М.: Наука, 1988.384 с.

13. Таха Х. А. Введение в исследование операций. 7-е изд. М.: Изд. дом Вильямс, 2005.

14.Фишберн П. С. Теория полезности для принятия решений. М.: Наука,1978. 352 с.

Подробнее..

Перевод Каждый браузер видит цвета видео по-разному

01.06.2021 10:11:39 | Автор: admin

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

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

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

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

Сегодня двумя самыми популярными цветовыми пространствами являются BT.601 (также называемое smpte170m; в статье я буду использовать оба этих названия), которое стало стандартом для SD-контента, и BT.709, которое стало стандартом HD-контента. Существует также BT.2020, которое становится популярнее благодаря HDR- и UHD-контенту. Стоит заметить, что разделение на HD/SD здесь немного ошибочно. Технические ограничения отсутствуют, это просто традиционный подход. HD-контент можно кодировать в BT.601, а SD-контент в BT.709. Если взять видеофайл с разрешением 1080p и уменьшить его до 480p, то цветовое пространство не изменится автоматически. Смена цветового пространства это дополнительный этап, который выполняется как часть процесса.

Что же происходит, если процесс выполняется неправильно? Давайте проведём эксперимент.

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

Для начала я создам с помощью ffmpeg простой тестовый файл:

ffmpeg -f rawvideo -s 320x240 -pix_fmt yuv420p -t 1 -i /dev/zero -an -vcodec libx264 -profile:v baseline -crf 18 -preset:v placebo -color_range pc -y 601.mp4

Краткое объяснение этой команды:

ffmpeg исполняемый файл

-f rawvideo сообщает программе ffmpeg, что я передаю ей сырые пиксельные данные, а не видеофайл.

-s 320x240 разрешение выходных данных. Можно использовать любое значение, потому что все входящие значения будут одинаковыми. Но благодаря маленькому размеру мы ускоряем процесс.

-pix_fmt yuv420p указываем формат пикселей входящих данных. Yuv420p это способ описания пикселей. Здесь важно отметить, что значение yuv(0,0,0) представляет собой оттенок зелёного. Я использую этот формат в противовес RGB, поскольку он является самым популярным форматом, используемым в цифровом видео.

-t 1 ограничиваем входящие данные 1 секундой

-i /dev/zero это файл входящих данных. /dev/zero это виртуальный файл, существующий на всех компьютерах mac. Это бесконечно длинный файл, состоящий из одних нулей.

-an обозначает, что выходные данные не должны содержать звука.

-vcodec libx264 используем для сжатия видео потрясающую библиотеку libx264.

-profile:v baseline используем базовый профиль h.264. Он отключает некоторые расширенные возможности h.264, но в этом тесте они нам не понадобятся.

-preset:v placebo сообщаем библиотеке libx264, что она может потратить дополнительные ресурсы процессора на кодирование видео с повышенным качеством. В реальной ситуации эту опцию выбирать не стоит, потому что кодирование занимает КУЧУ времени и обеспечивает минимальное улучшение качества. Нам она подходит, потому что у меня мало входящих данных.

-color_range pc один компьютерный байт может иметь значения от 0 до 255. При оцифровке аналогового видео используется интервал 16-235. Он был выбран из-за того, как телевизор интерпретирует очень тёмные и очень яркие сигналы. Так как мы используем цифровой источник, я выбрал значение pc, а не tv.

-crf 18 опция постоянного коэффициента потока (constant rate factor) сообщает libx264, что нужно создать высококачественный видеофайл и использовать любое количество бит, необходимое для обеспечения качества 18. Чем меньше число, тем выше качество. 18 это очень высокое качество.

-y даёт ffmpeg разрешение на перезапись файла, если он существует.

601.mp4 имя получившегося файла.

Эта команда создаёт файл 601.mp4 длительностью 1 секунду, который можно открывать и воспроизводить. После выполнения этой команды мы можем проверить, что ffmpeg не исказил значения пикселей, выполнив следующую команду и изучив выходные данные:

ffmpeg -i 601.mp4 -f rawvideo - | xxd

00000000: 0000 0000 0000 0000 0000 0000 0000 0000
00000010: 0000 0000 0000 0000 0000 0000 0000 0000
00000020: 0000 0000 0000 0000 0000 0000 0000 0000
00000030: 0000 0000 0000 0000 0000 0000 0000 0000
00000040: 0000 0000 0000 0000 0000 0000 0000 0000
00000050: 0000 0000 0000 0000 0000 0000 0000 0000
...
...

Эти данные в шестнадцатеричном виде показывают, что все значения пикселей после декодирования равны нулю.

При рендеринге видео в Safari мы получаем такой скриншот:


Возникает вопрос: что это за цветовое пространство? Я назвал файл 601.mp4, но нигде в команде я не указывал цветового пространства, так как же Safari узнал, какой оттенок зелёного нужно рендерить? Откуда браузер знает, что yuv(0,0,0) должно быть равно rgb(0,135,0)? Очевидно, что существует алгоритм для вычисления этих значений. На самом деле, это простое матричное умножение. (Примечание: в некоторых форматах пикселей, в том числе и в yuv420p, для преобразования требуется этап пре- и постпроцессинга, но в этой демонстрации мы опустим такие тонкости). Для каждого цветового пространства имеется собственная матрица. Так как мы не задавали матрицу цветового пространства при кодировании видео, Safari просто делает предположение. Мы можем перебрать все матрицы, умножить все значения RGB на обратные матрицы и посмотреть, чему же они соответствуют, но давайте попробуем использовать более визуальный подход и посмотреть, удастся ли нам разобраться, что делает Safari.

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

ffmpeg -f rawvideo -s 320x240 -pix_fmt yuv420p -t 1 -i /dev/zero -an -vcodec libx264 -profile:v baseline -crf 18 -preset:v placebo -colorspace smpte170m -color_primaries smpte170m -color_trc smpte170m -color_range pc -y 601vui.mp4

Команда ffmpeg осталась практически такой же, но я добавил следующее:

-color_trc smpte170m

-colorspace smpte170m

-color_primaries smpte170m


Это метаданные цветового пространства, в который будет кодироваться файл. Я не буду объяснять различия между этими опциями, потому что для этого понадобится ещё одна статья. Пока мы просто задаём всем им нужное нам цветовое пространство. smpte170m это то же самое, что и BT.601.

Указание цветового пространства не влияет на способ кодирования файла, значения пикселей по-прежнему кодируются как yuv(0,0,0). Чтобы убедиться в этом, мы можем выполнить для нового файла команду ffmpeg -i zero.mp4 -f rawvideo - | xxd. Флаги цветового пространства не игнорируются, однако просто записываются в несколько битов внутри раздела video usability information (VUI) в заголовке видеопотока. Теперь декодер будет искать VUI и использовать его для загрузки нужной матрицы.

А вот результат:


И с VUI, и без него видео рендерятся с одинаковым цветом. Давайте попробуем файл BT.709:

ffmpeg -i 601vui.mp4 -an -vcodec libx264 -profile:v baseline -crf 18 -preset:v placebo -vf "colorspace=range=pc:all=bt709" -y 709.mp4

Новые опции:

-i 601vui.mp4 используем в качестве источника видео прежний файл 601vui.mp4

-vf "colorspace=all=BT.709" сообщаем ffmpeg, что нужно использовать видеофильтр цветового пространства для изменения значений пикселей. Это похоже на умножение матрицы для преобразования из yuv в rgb, но матрица имеет другие коэффициенты. all это сокращение для одновременного задания color_primaries, colorspace и color_trc.

Здесь мы берём видео 601vui.mp4 и используем фильтр цветового пространства для преобразования в BT.709. Фильтр цветового пространства может считать из vui файла 601vui.mp4 цветовое пространство входящих данных, поэтому нам достаточно только указать цветовое пространство, которое мы хотим получить на выходе.

Выполнив для этого файла команду ffmpeg -i 709.mp4 -f rawvideo - | xxd, мы получаем после преобразования цветового пространства значения пикселей yuv(93,67,68). Однако при рендеринге файла он должен выглядеть так же. Стоит заметить, что окончательные результаты могут и не быть идентичными, потому что мы продолжаем использовать 24 бита для кодирования каждого пикселя, а BT.709 имеет чуть больший диапазон цветов. Следовательно, некоторые цвета в BT.709 не сопоставляются точно с BT.601, и наоборот.

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

image

Давайте внимательно изучим свойства файла при помощи приложения ffprobe:

ffprobe 601vui.mp4:
Stream #0:0(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuvj420p(pc, smpte170m), 320x240, 9 kb/s, 25 fps, 25 tbr, 12800 tbn, 50 tbc (default)

И

ffprobe 709.mp4:
Stream #0:0(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuvj420p(pc), 320x240, 5 kb/s, 25 fps, 25 tbr, 12800 tbn, 50 tbc (default)

Основная часть информации здесь для нас неважна, но мы заметим, что 601vui.mp4 имеет формат пикселей yuvj420p(pc, smpte170m). Так мы понимаем, что файл имеет правильный VUI. Но 709.mp4 содержит только yuvj420p(pc). Похоже, метаданные цветового пространства не были включены в выходной файл. Даже несмотря на то, что фильтр цветового пространства смог прочитать исходное цветовое пространство, и мы явным образом указали новое пространство, программа ffmpeg не записала правильный vui в конечный файл.

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

Обойти её можно, добавив метаданные цветового пространства вручную:

ffmpeg -i 601vui.mp4 -an -vcodec libx264 -profile:v baseline -crf 18 -preset:v placebo -vf "colorspace=range=pc:all=bt709" -colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range pc -y 709vui.mp4

В результате значение цвета в 709vui.mp4 будет равно rgb(0,132,0). Яркость зелёного канала чуть меньше, чем в 601vui.mp4, но поскольку преобразование цветового пространства происходит с потерями, а результат меня устраивает, то назовём это успехом.

Из этого мы можем прийти к заключению, что когда в файле не указано цветовое пространство, Safari считает, что это BT.601. И со стороны Safari это очень хорошее допущение. Но как сказано выше, BT.601 это стандарт SD-видео, а BT.709 стандарт для HD. Давайте проверим HD-видео с VUI и без него, и посмотрим, как их рендерит Safari. Я использовал те же команды ffmpeg, только изменил разрешение на 1920x1080.

image

И в SD, и в HD цвет рендерится одинаково. Делая предположение о цветовом пространстве, Safari не учитывает разрешение. Apple уже давно работает в пространстве медиа и издательского дела, поэтому я ожидал, что продукт этой компании обеспечит достойные результаты. Но если даже в Safari всё устроено настолько хитро, то интересно, как обстоит ситуация в других браузерах.

Chrome:


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

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


Изучив этот результат, мы увидим, что значения в 601 рендерятся очень похожими на предыдущие, но файлы 709 рендерятся так, как будто в них нет VUI. Из этого мы можем заключить, что Chrome при отключенном аппаратном ускорении просто игнорирует VUI и рендерит все файлы, как будто они имеют формат 601. Это означает, что все файлы 709 будут воспроизводиться некорректно.

И наконец, давайте изучим Firefox:


Здесь нужно разобрать многое. Так как 709.mp4 и 709vui.mp4 выглядят одинаково, можно прийти к заключению, что при отсутствии VUI браузер Firefox предполагает формат BT.709. Правильный рендеринг 601vui.mp4 означает, что для контента BT.601 раздел VUI учитывается. Однако когда файл BT.601 без VUI рендерится как 709, то становится очень тёмным. Очевидно, что невозможно отрендерить картинку правильно без всей необходимой информации, однако выбранный Firefox способ искажает цвет сильнее, чем выбранные браузерами Safari и Chrome.

Как мы видим, ситуация с рендерингом цветов видео по-прежнему представляет собой Дикий Запад. К сожалению, мы рассмотрели только часть проблемы. Мы ещё не изучали Windows. Давайте сделаем это.

Microsoft Edge:


Похоже, что Edge (по крайней мере, на моём компьютере) просто игнорирует VUI и рендерит всё как 601.

Chrome (со включенным аппаратным ускорением):


Ситуация не очень отличается от Mac. При наличии VUI он обрабатывается правильно, но если его нет, для SD-контента предполагается формат BT.601, а для HD-контента BT.709. Это единственный браузер, в котором я такое видел, но в этом есть определённая логика. Так как рендеринг выполняется иначе, чем на Mac, то подозреваю, что дело в ОС или, что более вероятно, в чём-то на уровне драйверов видеокарты, и этот выбор сделан не командой разработчиков Chrome.

Firefox ведёт себя так же, как и на Mac.


Что касается Linux, iOS, Android, Roku, Fire TV, смарт-телевизоров, игровых консолей и т.д., то я оставлю это в качестве упражнения для читателя.

Чему же мы научились? Самое главное: всегда указывайте в своих видео метаданные цветового пространства. Если вы пользуетесь ffmpeg и не задаёте флаги цвета, то вы работаете неправильно. Во-вторых, хотя ffmpeg и является потрясающей программой, её популярность, простота использования и неудачно выбранные стандартные параметры сослужили плохую службу. Никогда не стоит допускать, что ПО достаточно умно, чтобы разобраться в этом самостоятельно. Руководителям проектов Ffmpeg, Google, Mozilla, Microsoft (и, вероятно, Nvidia и AMD) нужно собраться и вместе выбрать единый способ. Я понимаю, что здесь нет хорошего решения, но плохое и предсказуемое лучше, чем плохое и случайное. Лично я рекомендую всегда предполагать формат BT.601, если раздел VUI отсутствует. Это создаёт наименьшую степень искажений. Можно выбрать для согласования этого стандарта FOMS, или даже AOM, поскольку эти организации имеют довольно неплохое представительство.

И напоследок: если у вас есть видео без информации о цвете и вам нужно его преобразовать или отрендерить, то удачи!



На правах рекламы


VDSina предлагает недорогие серверы с посуточной оплатой. Интернет-канал для каждого сервера 500 Мегабит, защита от DDoS-атак включена в тариф, возможность установить Windows, Linux или вообще ОС со своего образа, а ещё очень удобная панель управления серверами собственной разработки. Давно пора попробовать ;)

Присоединяйтесь к нашему чату в Telegram.

Подробнее..

Что такое алгоритм?? Part three and a quarter. Язык

10.06.2021 08:18:29 | Автор: admin

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


.

Title


Задача


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


Это Слово. Это Смысл. Это сложности перевода. Наконец, это разбор появления коммуникации.


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


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


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


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


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


И давайте обсуждать вместе. Ведь это "жжж" рядом с заголовком "Что такое алгоритм ?!" неспроста.


А зачем тебе жужжать, если ты не пчела? По-моему, так

А зачем на свете пчелы? Для того, чтобы делать мёд.

Жжж


И, конечно, "алгоритмический" мёд не очень интересен сказочному медвежонку, но, возможно, он станет полезен кому-то другому.


Слово


Пора уже начать. И в этот раз совсем нет причин изменять привычному для этой серии статей формату. Мы опять будем "терзать" общепринятые термины из Википедии. На нашем пути термин "Слово":


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

Как ни странно, но представленное определение весьма "крепкое", хоть пока и не формальное. А для выполнения его формализации необходима лишь малость. Нужно:


  • формализовать алгоритм именования предметов, взаимодействий и их качеств (характеристик);
  • понять и определить, что есть "мнимое и отвлеченное понятие", и затем формализовать алгоритм создания этой абстракции в человеческом воображении.

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


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


мультфильм головоломка


Коммуникация


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


Для синтеза алгоритма индивидуального поведения организма "Слово" не является таким уж необходимым. Но ограничение поведения только одним исполнителем слишком сильно и даже жестоко для живых организмов. И потому это ограничение совсем неприемлемо в нашей работе. Особенно когда вокруг нас такое множество примеров алгоритмов поведения коллективного. А вот для коллективных алгоритмов, которые исполняются несколькими особями совместно, "Слово" уже является обязательным и самым простым способом обеспечить "последовательность действий", а потому саму возможность коллективного исполнения. Да, это еще не то "Слово", определение которого указано в Википедии. Это "Протослово". Оно скорее уникальный синхронизирующий сигнал. И в этом сигнале (например, извещающем сородичей о приближающейся опасности) важна уникальность.


Ибо именно уникальность и коллективная идентифицируемость это первооснова любого "Слова"? Если бы сигнал "Опасность" и сигнал "Вкусная еда" были бы одинаковы, то алгоритмы избегания стадом хищника работали бы совсем неудовлетворительно, и в результате исчезло бы и стадо, а вместе со стадом и выполняемые им алгоритмы.


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


Итак, есть мостик от алгоритмов коллективного выживания к алгоритмам "именования предметов". Почему здесь использовано слово "алгоритмам" во множественном числе? Это связано с разной природой "именуемых" явлений. И совсем немного этому посвящена пара статей "Математика" и "Физика". Там намеренно отдельно в каждой статье рассмотрены особенности "переноса" в пространство модели для "стада коров" и для "падения яблока".


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


Именование трансляцией


Абстракция


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


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


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


Итак, на основе рассмотренного ранее мы с Вами уже можем еще неформально, но в формальных терминах (отмеченных курсивом) теоретической части этой работы, сформулировать своё определение "Слова":


Слово это объект в пространстве языковой алгоритмической системы, который:
  • формируется с использованием модельной трансляции (например, человеком) комплементарным сопоставлением с объектом или алгоритмом из прикладной области;
  • создается как продукт работы алгоритмов в языковом пространстве, необходимый для алгоритмов, исполняемых только внутри языкового пространства, и не переносимый в прикладную область.

Ведь очень похоже на определение из Вики? Неясными остаются только несколько терминов, которые ещё не были упомянуты в предыдущих статьях. Но все эти термины уже есть в теоретической части. И самый интересный из них "Алгоритмическая система". В определении "Слова" мимоходом констатируется, что "Язык" это один из видов "Алгоритмической системы". К сожалению, в текущей статье этот комплексный термин не уместить, а потому пока оставим его для статей последующих.


Копирование поведения


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


Смысл


Здесь можно немного поумничать и для поощрения дальнейшей непростой работы при чтении остатка текущей статьи легко ответить на вопрос "Что есть смысл жизни?" простой фразой: "Смысл любого слова кроется в алгоритмах, в которых это слово используется". Конечно, этот ответ, не подкрепленный объяснениями, совсем как красная ткань в корриде. И потому уместно использовать это и напасть на этот ответ в комментариях. Но объяснения к этой фразе уже есть. Они основаны на предлагаемом в этой серии статей значении для слова "Алгоритм", которое еще не настолько общепринято, чтобы его можно было использовать совсем без примеров.


Question of life


$\begin{gathered}42 \\ = \\ (-80538738812075974)^3 + \\ 80435758145817515^3 + \\ 12602123297335631^3\end{gathered}$



И если в текущий момент статьи сделать паузу и обратить внимание на предыдущее предложение, а еще сразу на все статьи серии. То можно отметить, что цель всего этого собрания слов в статьях только одна объяснить особый смысл слова "Алгоритм". А способ это осуществить используется такой же, как и для всех иных слов, с которыми сталкивается человек. Начиная с первого слова "мама" и заканчивая "большим адронным коллайдером" (да, конечно, тут 3 слова, но ведь смысл один?). И этот способ связывания слова со смыслом спрятан достаточно глубоко в устройстве человеческого мышления, а вернее в приемах используемых в процессе синтеза алгоритмов его поведения.


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


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


Но есть ли в этом "классифицирующем" пути к "Слову" способ понять его смысл? Ответ будет да. Но как уже было сказано, это будет только часть смысла. И эта часть отнюдь не самая важная. И эта часть уже есть в текущем определении слова "Алгоритм", размещаемом на Википедии. И она помогает нам отличить "Алгоритм", например, от "Логарифма" потому, что при работе с алгоритмом мы не всегда думаем о "показателе степени, в которую надо возвести основание". Но эта часть совсем не помогает нам работать с Алгоритмом! Обобщая можно подчеркнуть: путь разбиения явлений среды на кластеры, каждый из которых подписан уникальным словом дает нам представление только о части смысла, содержащегося в этих уникальных словах.


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


Главный смысл "Слова" содержится в полезных алгоритмах, которые используют это слово! Да, смысл совсем не внутри фонем, букв, слогов "слова" и не в способах именовать объекты окружающего пространства. Этот смысл "снаружи"! Он в использовании. Он в алгоритмах!!!


Тут будет уместно сбавить градус повествования и в качестве интересного факта подчеркнуть, что для слов, представляющих собой "мнимые и отвлеченные понятия" особенно из математики (например, для "мнимой единицы"), баланс между "классифицирующим" смыслом и смыслом "использования" нарушен изначально. Алгоритмы идентифицирующие "мнимую единицу" (классифицирующий смысл) намеренно делаются очень простыми и сводятся к распознаванию одной буквы $i$, которая почти ничего не значит, но только "существует" в математическом выражении. А вот "использующий смысл" очень насыщенный. Это и алгоритмы решения кубического уравнения, и комплексные числа, и далее согласно Википедии использование: в обработке сигналов, теории управления, электромагнетизме, теории колебаний, теории упругости, картографии и гидродинамике и даже в квантовой механике.


Для "гуманитарных" слов порой отсутствует "использующий смысл". И слово становится просто интересным алгоритмом, необходимым только для развлечения того кто его слышит. Вспомним для примера "Антиподов" упомянутых Алисой в предыдущей статье. До появления использования этого слова в статье "Физика" оно было просто развлечением и игрой Льюиса Кэрролла (или даже скорее переводчика его сказки на русский язык). Это было ироничное издевательство над алгоритмом "именования" с копированием того, как в своих играх этот алгоритм изучают дети. Но ситуация поменялась с появлением текущей статьи. В ней появилось новое использование слова "Антипод" в качестве лингвистического примера. И у этого "Слова" появился новый смысл! Ведь это совсем неожиданный для математика процесс?


Алиса и Аня


При обращении к примеру сказочной Алисы Льюиса Кэрролла был вскользь упомянут один сложный процесс, который целиком основан на смысле "Слова" и потому долгое время остаётся не до конца подвластен Математике. И этот процесс не может происходить только на основе "классифицирующего смысла". И он нуждается в "смысле использования". Вы уже догадались? Да, этот процесс работа переводчика, нацеленная на перенос смысла "Слова" из одного языка в язык другой.


Перевод


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


Думаю сейчас опять стоит обратиться к Википедии. И разобрать следующий существующий на её страницах термин слово "Перевод":


Перевод деятельность по интерпретации смысла текста на одном языке (исходном языке) и созданию нового эквивалентного ему текста на другом языке (переводящем языке).

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


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


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


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


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


Кузинатра


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


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


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


Выводы


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


Мы познакомились с термином "Слово" и нашли в нём два смысла, один из которых будет весьма полезным в доработке существующих систем перевода текстов на естественных языках.


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


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


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


Да, тут необходимо отметить, что все обозначенные в этой статье формализации очень "неформальны". И в них намеренно только обозначены опорные и ключевые моменты. А потому, конечно, пропущено много деталей, и их необходимо заполнить. И только частично это заполнение будет проводиться в оставшемся небольшом количестве статей этой серии. И только самые важные пояснения появятся на Хабре в автономных статьях. А до полного их объяснения в разрабатываемой теоретической книге пройдет еще очень много времени потому, что эта работа совсем не является сейчас самой приоритетной. И потому "атаку на пробелы" и их заполнение целесообразно разместить здесь (на Хабре). Удобной платформой для этого будет площадка комментариев и к этой, и к предыдущим статьям. И это заполнение будет идти только по Вашим запросам. Ибо пробелы, которые очень хорошо видны Вам, мне не видны совершенно. Отчасти поэтому в статьях появляются "висящие" термины. И в написании каждой новой статьи самым сложным становится процесс вспоминания: какие факты уже были рассказаны, а какие еще остались такими "пробелами". Ведь полный путь от "рождения" алгоритма к способам работы нашего мозга всё же достаточно сложен. И держать его целиком в памяти, к сожалению, никогда не будет мне посильно.


Спасибо Вам за внимание.


Отзывы


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


Отдельное волнение у меня есть по стилю повествования и форматированию, используемым в статье (кавычки, абзацы, курсив). Напишите, пожалуйста, если у Вас есть замечания к ним. Можно личным сообщением.


Ссылки


Подробнее..

Перевод Либо быстро, либо неправильно

02.06.2021 18:06:54 | Автор: admin
image


В 2018 году я упражнялся на Advent of Code (здесь вы можете посмотреть стримы моих решений). Каждый день в декабре они публикуют небольшую проблему, и вы должны написать программу, которая её решит. Обычно это занимает от пары минут до пары часов и это довольно весело, я рекомендую вам попробовать. Когда задача выполнена, она всегда доступна, не только в декабре.

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

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

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

Теперь перейдем к программному обеспечению. Легко назвать решения Advent Of Code ошибочными, когда они медленные, поскольку мы знаем, что быстрое решение должно существовать. С реальными проблемами никто этого не гарантирует.

За исключением некоторых случаев.

Собственно, довольно часто.

На самом деле, я бы сказал, почти всегда.

Давайте посмотрим. У меня есть библиотека под названием Datascript. Это устойчивая структура данных/база данных и так уж вышло, что она реализована для двух платформ: JS и JVM. Более того, она фактически написана на Clojure, и большая часть ее кодовой базы используется обеими платформами. Это означает, что мы знаем, что обе версии всегда совершают одни и те же действия. Есть небольшой слой, который покрывает специфичные для платформы детали, такие как типы данных и стандартная библиотека, но остальное является общим. Дело не в том, что одна версия является оригинальной, а другая неэффективным портом. Они обе играют в одну игру.

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

Давайте посмотрим на фактическое время, необходимое для компиляции кодовой базы и выполнения полного набора интеграционных тестов. Мы говорим о кодовой базе, которая составляет чуть более 9000 LOC, из которых 4000 тестов:

Clojure 1.10 on JVM:

  • REPL boot time: 1.5 sec
  • Compile time: 6.5 sec
  • Tests time: 0.45 sec


ClojureScript 1.10.439 with advanced compilation:

  • Compile time: 78 sec
  • Tests time: 1 sec


ClojureScript 1.10.439 without Google Closure compilation:

  • Compile time: 24 sec
  • Tests time: 1.3 sec


Итак, о чем нам говорят эти числа? По сути, для обработки одного и того же кода вы можете потратить ~8 секунд, 24 секунды или 78 секунд. Выбор за вами. Кроме того, запустив ту же программу, вы можете получить результат за полсекунды, одну секунду или почти полторы секунды.

Но подожди, Tonsky, их нельзя сравнивать! Это две большие разницы! Они созданы, чтобы делать совершенно разные вещи! Один из них работает в браузере!

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

Что так долго делают компиляторы ClojureScript/Google Closure? Они зря тратят ваше время, вот что. Конечно, никто не виноват, но, в конце концов, все это решение просто неверно. Мы можем делать то же самое намного быстрее, у нас есть доказательства, у нас есть средства для этого, но так уж получилось, что это не так. Но мы могли бы. Если бы захотели. Эти огромные накладные расходы, которые вы платите, вы платите зря. Вы ничего не получаете от JS, кроме удвоения времени выполнения и астрономического времени сборки.

То же самое относится ко всем языкам с ужасно долгим временем сборки. Дело не в том, что они не могли бы работать быстрее. Они просто предпочитают не делать этого. Программа на C ++ или Rust слишком долго компилируется? Что ж, OCaml, вероятно, мог бы скомпилировать эквивалентную программу менее чем за секунду. И это по-прежнему будет быстро на уровне машины.

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

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

Представьте себе: из Москвы в Новосибирск, сердце Сибири, летит самолет, который преодолевает 2800 километров за 4 часа. Еще есть поезд, который преодолевает такое же расстояние за три дня. В поезде нет душа, плохая еда, кровати, на которых нельзя спать. А самолет это комфортабельный современный самолет. Что бы вы выбрали? Цена такая же. Единственная разница это ваш комфорт и ваше время.

image

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

Браузеры? Та же история. HTML довольно неэффективный способ разместить пиксели на экране. Компьютер, который мог бы отображать миллионы полигонов в кадре, может с трудом прогружать веб-страницу. Как и в случае с решениями Advent of Code, это не зависит от мощности вашего компьютера. И даже высокооптимизированный веб-код, основанный на Canvas и WebAssembly (Figma), заставляет вентиляторы моего Macbook крутиться при полной тишине при запуске собственного Sketch.

image

*похлопывает по крышке* Этот ПК способен запустить Crysis 3 в 4K на 144fps.
Но может ли он запустить Atom?


Просто существуют пределы того, насколько далеко может зайти это неправильное решение. Текстовые редакторы на Electron не могут менять размер окна в реальном времени и проседают по кадрам, когда вы просто двигаете курсором. Slack на iMac Pro будет таким же медленным и требовательным к памяти, как и на 12-дюймовом Macbook.

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

Я могу продолжать и продолжать. Следует помнить следующее: подумайте, что вы от этого получаете. Сопоставимы ли проблема и потраченные на нее ресурсы? Легко найти оправдания тому, почему дела обстоят так, как есть. Все они, вероятно, действительны, но это отговорки. Мы знаем, что возможны более быстрые программы, и это делает все остальное просто неправильным.
Подробнее..

Процессор, эмулирующий сам себя может быть быстрее самого себя

04.06.2021 04:18:40 | Автор: admin

Современный мир ПО содержит настолько много слоёв, что оптимизации могут быть в самых неожиданных местах. Знакомьтесь - год 2000, проект HP Dynamo. Это эмулятор процессора PA-8000, работающий на этом же процессоре PA-8000, но с технологией JIT. И реальные программы, запускающиеся в эмуляторе - в итоге работают быстрее, чем на голом процессоре.

td;dr - всё сказано в заголовке

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

В эмуляторе они искали "hot paths" и оптимизировали ход исполнения кода. Таким образом уменьшались расходы на джампы, вызов функций, динамических библиотек, оптимизации работы с кешем процессора. Результаты повышения производительности доходили до +22%, в среднем по тестам получалось +9%.

Эта короткая заметка написана, чтобы кто-нибудь мог узнать что-то новое, а в совсем технические детали 20-летней давности смысла лезть нет.

Если кому интересны подробности:

1. http://cseweb.ucsd.edu/classes/sp00/cse231/dynamopldi.pdf
2. https://stackoverflow.com/questions/5641356/why-is-it-that-bytecode-might-run-faster-than-native-code/5641664#5641664
3. https://en.wikipedia.org/wiki/Just-in-time_compilation

Подробнее..

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

16.06.2021 22:19:34 | Автор: admin

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

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

В результате родился проект Объясняем код. Посмотреть, что это такое можно на code-explained.com. Код проекта выложен на Гитхаб.

Чем я вдохновлялся

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

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

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

Как сегодня изучают алгоритмы

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

В видео на Youtube происходит нечто подобное ведущий берет код алгоритма и отрисовывает этапы его работы

На ИТ-ресурсах создают анимации.

А кто-то даже пытается объяснить работу алгоритма через танец.

Почему эти подходы казались мне неэффективными

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

Как я перешел от технозависимости к человечности

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

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

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

Как это технически реализовано

Для кода на Python написан аналогичный код на JS, который после выполнения каждой строчки сохраняет состояние. Такой трюк помогает исполнять код на фронтенде без тяжелого интерпретатора. Чтобы убедиться, что код на JS делает то же самое, что и на Python, есть автотестер он прогоняет код на обоих языках и сравнивает результаты.

Чтобы сэкономить на копировании данных, я применил Immutable.js. Immutable.js это библиотека неизменяемых коллекций. Модификации таких коллекций всегда возвращают новую коллекцию, которую легко сохранить. Так я избегаю глубокого копирования. Не буду вдаваться в подробности, на Хабре про immutable.js уже писали. Для примера кусочек кода с итерированием по хеш-таблице:

while (true) { // Итерируемся по списку    this.addBP('check-not-found'); // Метод сохраняем состояние    if (this.newList.get(this.newListIdx) === null) {        // this.newList -- это немутабельный список        break;    }    this.addBP('check-found'); // Выполнена очередная строчка, сохраняем состояние    if (EQ(this.newList.get(this.newListIdx), this.number)) {        this.addBP('found-key');        return true;    }    this.fmtCollisionCount += 1; // Для динамических комментариев иногда нужно сохранять статистикуу    this.newListIdx = (this.newListIdx + 1) % this.newList.size; // Переходим к следующему индекксу    this.addBP('next-idx');}

В любой момент пользователь может отмотать время слайдером, и мы должны быть готовы анимировать переход. Поэтому анимации сделаны декларативно на React и CSS. На React описываются нужное состояние, а CSS transitions автоматически высчитывают и анимируют переход. Это позволяет избежать совсем сложного императивного кода с запутанным состоянием и ручными вычислениями. Получается тоже не просто, и в итоге анимациями заведует огромный класс на 1000 строк. Но я уверен, что императивно реализовать то же самое у меня вообще не получилось бы.

Много строк возникает из-за особенностей браузерных API и производительности в разных браузерах. Например, большой проблемой оказалось сделать так, чтобы браузеры не склеивали последовательные изменения друг с другом. Если добавить div с определённой начальной позицией, и потом сразу же поменять координаты на конечные, то браузер склеит эти два изменения в одно. Div сразу окажется в конечной позиции без анимации. Чтобы такое не происходило, приходится вставлять задержку в два фрейма анимации с помощью window.requestAnimationFrame().

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

Код проекта на гитхабе

Что дальше?

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

Подробнее..

Powershell настоящий язык программирования. Скрипт оптимизации рутины в техподдержке

20.06.2021 14:08:21 | Автор: admin

Работая в компании IT-аутсорса в качестве руководителя 3 линии поддержки, задумался, как автоматизировать подключение сотрудников по RDP, через VPN к серверам десятков клиентов.

Таблички с адресами, паролями и прочими настройками серверов, конечно, хорошо, но поиск клиента и вбивание адресов с аккаунтами занимает довольно существенное время.
Держать все подключения к VPN в Windows не самая лучшая идея, да и при переустановке оного, создавать VPNы тоже не доставляет удовольствие.
Плюс к тому, в большинстве случаев, требуется установить VPN подключение к клиенту без использования шлюза. дабы не гонять весь интернет-трафик через клиента.
Задача, к тому же, осложняется тем, что у некоторых клиентов pptp, у кого-то l2tp, у некоторых несколько подсетей, туннели и т.п.

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

До написания этого скрипта-приложения программированием не занимался вообще, разве что лет 20 назад что-то пописывал на VBS в MS Excel и MS Access, поэтому не гарантирую красивость кода и принимаю критику от опытных программистов, как можно было бы сделать красивее.

В Powershell, начиная с Windows 8 и, конечно в Windows 10, появилась прекрасная возможность создавать VPN подключения командой Add-VpnConnection и указывать какие маршруты использовать с этими соединениями командой Add-VpnConnectionRoute, для использования VPN без шлюза.

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

Для начала, создаем в Google Disk таблицу с именованными столбцами:
Number; Name; VPNname; ServerAddress; RemoteNetwork; VPNLogin; VPNPass; VPNType; l2tpPsk; RDPcomp; RDPuser; RDPpass; DefaultGateway; PortWinbox; WinboxLogin; WinboxPwd; Link; Inform

  • VPNname произвольное имя для VPN соединения

  • ServerAddress адрес VPN сервера

  • RemoteNetwork адреса подсети или подсетей клиента, разделенные ;

  • VPNLogin; VPNPass учетная запись VPN

  • VPNType -тип VPN (пока используется pptp или l2tp)

  • l2tpPsk PSK для l2tp, в случае pptp оставляем пустым

  • RDPcomp адрес сервера RPD

  • RDPuser; RDPpass учетная запись RPD

  • DefaultGateway принимает значение TRUE или FALSE и указывает на то, использовать ли Шлюз по умолчанию для этого соединения. В 90% случаев = FALSE

  • PortWinbox; WinboxLogin; WinboxPwd порт, логин и пароль для Winbox, поскольку у нас большинство клиентов использует Mikrotik)

  • Link ссылка на расширенную информацию о компании, например, на диске Google, или в любом другом месте, будет выводиться в информационном поле для быстрого доступа к нужной информации

Inform примечание

Пример таблицы доступен по ссылке

Number

Name

VPNname

ServerAddress

RemoteNetwork

VPNLogin

VPNPass

VPNType

l2tpPsk

RDPcomp

RDPuser

RDPpass

DefaultGateway

PortWinbox

WinboxLogin

WinboxPwd

Link

Inform

1

Тест1

Test1

a.b.c.d

192.168.10.0/24: 10.10.0.0/24

vpnuser

passWord

pptp

none

192.168.10.1

user

passWord

TRUE

8291

Admin

Admin

http://yandex.ru

тест

2

Тест2

Test2

e.f.j.k

192.168.2.0/24

vpnuser

passWord

l2tp

KdoSDtdP

192.168.2.1

user

passWord

FALSE

8291

Admin

Admin

Скриншот работающего приложения с затертыми данными:

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

function Get-Clients #Функция принимает строку адреса файла в Google Drive и возвращает в виде массива данных о клиентах{param([string]$google_url = "")[string]$xlsFile = $google_url$csvFile = "$env:temp\clients.csv"$Comma = ','Invoke-WebRequest $xlsFile -OutFile $csvFile$clients = Import-Csv -Delimiter $Comma -Path "$env:temp\clients.csv"Remove-Item -Path $csvFilereturn $clients}function Main {<#    Функция, срабатываемая при запуске скрипта#>Param ([String]$Commandline)#Иннициализируем переменные и присваиваем начальные значения. Здесь же, указываем путь к таблице с клиентами$Global:Clients = $null$Global:Current$Global:CurrentRDPcomp$Global:google_file = "https://docs.google.com/spreadsheets/d/1O-W1YCM4x3o5W1w6XahCJZpkTWs8cREXVF69gs1dD0U/export?format=csv" # Таблица скачивается сразу в виде csv-файла$Global:Clients = Get-Clients ($Global:google_file) # Присваиваем значения из таблицы массиву #Скачиваем Winbox64 во временную папку$download_url = "https://download.mikrotik.com/winbox/3.27/winbox64.exe"$Global:local_path = "$env:temp\winbox64.exe"If ((Test-Path $Global:local_path) -ne $true){$WebClient = New-Object System.Net.WebClient$WebClient.DownloadFile($download_url, $Global:local_path)}  #Разрываем все текущие VPN соединения (на всякий случай)foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){Rasdial $item.Name /disconnect}  #Удаляем все, ранее созданные программой временные соединения, если вдруг не удалились при некорректном закрытии приложенияget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force#Запускаем приложениеShow-MainForm_psf}#Собственно, само приложениеfunction Show-MainForm_psf{[void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')[void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')#Создаем форму и объекты формы[System.Windows.Forms.Application]::EnableVisualStyles()$formКлиентыАльбус = New-Object 'System.Windows.Forms.Form'$statusbar1 = New-Object 'System.Windows.Forms.StatusBar'$groupboxTools = New-Object 'System.Windows.Forms.GroupBox'$buttonPing = New-Object 'System.Windows.Forms.Button'$buttonВыход = New-Object 'System.Windows.Forms.Button'$buttonWindox = New-Object 'System.Windows.Forms.Button'$buttonПеречитатьДанные = New-Object 'System.Windows.Forms.Button'$buttonPingAll = New-Object 'System.Windows.Forms.Button'$groupboxRDP = New-Object 'System.Windows.Forms.GroupBox'$comboboxRDP = New-Object 'System.Windows.Forms.ComboBox'$textboxRDPLogin = New-Object 'System.Windows.Forms.TextBox'$textboxRdpPwd = New-Object 'System.Windows.Forms.TextBox'$buttonПодключитьRDP = New-Object 'System.Windows.Forms.Button'$groupboxVPN = New-Object 'System.Windows.Forms.GroupBox'$buttonПодключитьVPN = New-Object 'System.Windows.Forms.Button'$buttonОтключитьVPN = New-Object 'System.Windows.Forms.Button'$checkboxШлюзПоумолчанию = New-Object 'System.Windows.Forms.CheckBox'$richtextboxinfo = New-Object 'System.Windows.Forms.RichTextBox'$listbox_clients = New-Object 'System.Windows.Forms.ListBox'$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'  #----------------------------------------------# Обработчики событий#----------------------------------------------$formКлиентыАльбус_Load = {#При загрузке формы очистить поле информации и заполнить поле с клиентами (их названиями) $richtextboxinfo.Clear()$Global:Clients | ForEach-Object {[void]$listbox_clients.Items.Add($_.Name)} # В листбокс добавляем всех наших клиентов по именам и массива при загрузке формы}$listbox_clients_SelectedIndexChanged = {#Прочитать из массива информацию о клиенте при выборе его в поле listbox_clients (массив, как мы помним считан из файла с диска Google)$statusbar1.Text = 'Выбран клиент: ' + $listbox_clients.SelectedItem.ToString() # Пишем клиента в статусбар$Global:Current = $Global:Clients.Where({ $_.Name -eq $listbox_clients.SelectedItem.ToString() })If ($Current.PortWinbox -ne 0) # Если порт Winbox указан, то у клиента Mikrotik, включаем соответствующую кнопку{$buttonWindox.Enabled = $true$buttonWindox.Text = "Winbox"}$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только ихswitch ($Global:Current.VPNType) #В зависимости от типа VPN пишем на кнопке "Подключить pptp VPN" или "Подключить l2tp VPN", если у клиента нет VPN, то пишем "Здесь нет VPN"{"pptp" {$buttonПодключитьVPN.Enabled = $true$buttonПодключитьVPN.Text = "Подключить pptp VPN"}"l2tp" {$buttonПодключитьVPN.Enabled = $true$buttonПодключитьVPN.Text = "Подключить l2tp VPN"}DEFAULT{$buttonПодключитьVPN.Enabled = $false$buttonПодключитьVPN.Text = "Здесь нет VPN"}}switch ($Global:Current.DefaultGateway) #Смотрим в массиве, используется ли у клиента "Шлюз по-умолчанию" и заполняем соответствующий чекбокс{"FALSE"{ $checkboxШлюзПоумолчанию.Checked = $false }"Нет"{ $checkboxШлюзПоумолчанию.Checked = $false }"TRUE"{ $checkboxШлюзПоумолчанию.Checked = $true }"Да"{ $checkboxШлюзПоумолчанию.Checked = $true }DEFAULT{ $checkboxШлюзПоумолчанию.Checked = $false }}$VPNStatus = (ipconfig | Select-String $VPNname -Quiet) #Проверяем, не установлено ли уже это VPN соединение?If ($VPNStatus) #Если установлено, то разблокируем кнопку "Подключить RDP"{$buttonПодключитьRDP.Enabled = $true}else{$buttonПодключитьRDP.Enabled = $false}$richtextboxinfo.Clear() #Очищаем информационное поле # И заполняем информацией о клиенте из массива$richtextboxinfo.SelectionColor = 'Black'$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLine + `"Имя VPN: " + $Global:Current.VPNname + [System.Environment]::NewLine + `"Тип VPN: " + $Global:Current.VPNType + [System.Environment]::NewLine + `"Адрес сервера: " + $Global:Current.ServerAddress + [System.Environment]::NewLine + `"Подсеть клиента: " + $Global:Current.RemoteNetwork + [System.Environment]::NewLine + `"Адрес сервера RDP: " + $Global:Current.RDPcomp + [System.Environment]::NewLine + [System.Environment]::NewLine + `"DefaultGateway: " + $Global:Current.DefaultGateway + [System.Environment]::NewLine + [System.Environment]::NewLine + `"Примечание: " + [System.Environment]::NewLine + $Global:Current.Inform + [System.Environment]::NewLine + `"Connection '" + $VPNname + "' status is " + $buttonПодключитьRDP.Enabled + [System.Environment]::NewLine$richtextboxinfo.AppendText($Global:Current.Link)$RDPServers = $Global:Current.RDPcomp.Split(';') -replace '\s', '' #Считываем и разбираем RDP серверы клиента из строки с разделителем в массив#Добавляем из в выпадающее поле выбора сервера$comboboxRDP.Items.Clear()$comboboxRDP.Text = $RDPServers[0]foreach ($RDPServer in $RDPServers){$comboboxRDP.Items.Add($RDPServer)}#Заполняем поля имени и пароля RDP по умолчанию из таблицы о клиенте (при желании, их можно поменять в окне программы)$textboxRdpPwd.Text = $Global:Current.RDPpass$textboxRdpLogin.Text = $Global:Current.RDPuser} # Форма заполнена, при смене выбранного клиента произойдет перезаполнение полей в соответствии с выбранным клиентом$buttonWindox_Click = {#Обработка нажатия кнопки WinboxIf ($Global:Current.PortWinbox -ne 0) #Если порт Winbox заполнен, то открываем скачанный ранее Winbox, подставляем туда имя и пароль к нему и запускаем{$runwinbox = "$env:temp\winbox64.exe"$ServerPort = $Global:Current.ServerAddress + ":" + $Global:Current.PortWinbox$ServerLogin = " """ + $Global:Current.WinboxLogin + """"$ServerPass = " """ + $Global:Current.WinboxPwd + """"$Arg = "$ServerPort $ServerLogin $ServerPass "Start-Process -filePath $runwinbox -ArgumentList $Arg}}$buttonПодключитьVPN_Click = {#Обработка нажатия кнопки ПодключитьVPN$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только их$richtextboxinfo.Clear() #Очищаем информационное поля для вывода туда информации о процессе подключения$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLineforeach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }) #Разрываем все установленные соединения{$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}Remove-VpnConnection $VPNname -Force #Удаляем соединение, если ранее оно было создано$RemoteNetworks = $Global:Current.RemoteNetwork.Split(';') -replace '\s', '' #Считываем и разбираем по строкам в массив список подсетей клиента разделенный ;switch ($Global:Current.VPNType) #В зависимости от типа VPNа создаем pptp или l2tp соединение{"pptp" {$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем pptp подключение " + $VPNname + [System.Environment]::NewLineIf ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPNforeach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN{$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru}}else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -Force -RememberCredential -PassThru)}}"l2tp" {$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем l2tp подключение " + $Global:Current.VPNname + [System.Environment]::NewLineIf ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPNforeach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN{$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru}}else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -Force -RememberCredential -PassThru)}}}$richtextboxinfo.AppendText("Устанавливаем " + $Global:Current.VPNType + " подключение к " + $VPNname + [System.Environment]::NewLine)$Errcon = Rasdial $VPNname $Global:Current.VPNLogin $Global:Current.VPNPass #Устанавливаем созданное VPN подключение и выводим информацию в поле$richtextboxinfo.Text = $richtextboxinfo.Text + [System.Environment]::NewLine + $Errcon + [System.Environment]::NewLineIf ((ipconfig | Select-String $VPNname -Quiet)) #Проверяем успешность соединения и, если все удачно, разблокируем кнопку RDP  и кнопку "Отключить VPN"{$buttonПодключитьRDP.Enabled = $true$buttonОтключитьVPN.Visible = $true$buttonОтключитьVPN.Enabled = $true$statusbar1.Text = $Global:Current.Name + ' подключен'}}$formКлиентыАльбус_FormClosing = [System.Windows.Forms.FormClosingEventHandler]{#При закрытии формы подчищаем за собой. Разрываем и удаляем все созданные соединения. foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLineget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force#Удаляем информацию о RPD-серверах из реестра$Global:Clients | ForEach-Object {$term = "TERMSRV/" + $_.RDPcompcmdkey /delete:$term}}$buttonПодключитьRDP_Click = {#Обработка кнопки ПодключитьRDP$RDPcomp = $comboboxRDP.Text$RDPuser = $textboxRDPLogin.Text$RDPpass = $textboxRdpPwd.Textcmdkey /generic:"TERMSRV/$RDPcomp" /user:"$RDPuser" /pass:"$RDPpass"mstsc /v:$RDPcomp}$buttonОтключитьVPN_Click = {#При отключении VPN подчищаем за собой и оповещаем о процессе в поле информацииforeach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLineget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force$buttonОтключитьVPN.Visible = $false$buttonПодключитьRDP.Enabled = $false$statusbar1.Text = $Global:Current.Name + ' отключен'}$buttonPingAll_Click={#Пингуем всех клиентов и оповещаем о результатах$I=0$richtextboxinfo.Clear()$richtextboxinfo.SelectionColor = 'Black'$clientscount = $Global:Clients.count$Global:Clients | ForEach-Object {if ((test-connection -Count 1 -computer $_.ServerAddress -quiet) -eq $True){$richtextboxinfo.SelectionColor = 'Green'$richtextboxinfo.AppendText($_.Name +' ('+ $_.ServerAddress +') доступен' + [System.Environment]::NewLine)}else{$richtextboxinfo.SelectionColor = 'Red'$richtextboxinfo.AppendText($_.Name + ' (' + $_.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)}$richtextboxinfo.ScrollToCaret()$I = $I + 1Write-Progress -Activity "Ping in Progress" -Status "$i clients of $clientscount pinged" -PercentComplete ($i/$clientscount*100)}$richtextboxinfo.SelectionColor = 'Black'Write-Progress -Activity "Ping in Progress" -Status "Ready" -Completed}$buttonПеречитатьДанные_Click={#Перечитываем данные из таблицы Google$Global:Clients = Get-Clients ($Global:google_file)$listbox_clients.Items.Clear()$Global:Clients | ForEach-Object {[void]$listbox_clients.Items.Add($_.Name)}}$buttonВыход_Click = {#Выход$formКлиентыАльбус.Close()}$richtextboxinfo_LinkClicked=[System.Windows.Forms.LinkClickedEventHandler]{#Обработка нажатия на ссылку в окне информацииStart-Process $_.LinkText.ToString()}$buttonPing_Click={#Пингуем ip текущего клиента и выводим результат в поле информацииif ((test-connection -Count 1 -computer $Global:Current.ServerAddress -quiet) -eq $True){$richtextboxinfo.AppendText([System.Environment]::NewLine)$richtextboxinfo.SelectionColor = 'Green'$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ') доступен' + [System.Environment]::NewLine)}else{$richtextboxinfo.AppendText([System.Environment]::NewLine)$richtextboxinfo.SelectionColor = 'Red'$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)}}#----------------------------------------------#Описание объектов формы#----------------------------------------------## formКлиентыАльбус#$formКлиентыАльбус.Controls.Add($statusbar1)$formКлиентыАльбус.Controls.Add($groupboxTools)$formКлиентыАльбус.Controls.Add($groupboxRDP)$formКлиентыАльбус.Controls.Add($groupboxVPN)$formКлиентыАльбус.Controls.Add($richtextboxinfo)$formКлиентыАльбус.Controls.Add($listbox_clients)$formКлиентыАльбус.AutoScaleDimensions = '6, 13'$formКлиентыАльбус.AutoScaleMode = 'Font'$formКлиентыАльбус.AutoSize = $True$formКлиентыАльбус.ClientSize = '763, 446'$formКлиентыАльбус.FormBorderStyle = 'FixedSingle'$formКлиентыАльбус.MaximizeBox = $False$formКлиентыАльбус.Name = 'formКлиентыАльбус'$formКлиентыАльбус.SizeGripStyle = 'Hide'$formКлиентыАльбус.StartPosition = 'CenterScreen'$formКлиентыАльбус.Text = 'Клиенты Альбус'$formКлиентыАльбус.add_FormClosing($formКлиентыАльбус_FormClosing)$formКлиентыАльбус.add_Load($formКлиентыАльбус_Load)## statusbar1#$statusbar1.Location = '0, 424'$statusbar1.Name = 'statusbar1'$statusbar1.Size = '763, 22'$statusbar1.TabIndex = 17## groupboxTools#$groupboxTools.Controls.Add($buttonPing)$groupboxTools.Controls.Add($buttonВыход)$groupboxTools.Controls.Add($buttonWindox)$groupboxTools.Controls.Add($buttonПеречитатьДанные)$groupboxTools.Controls.Add($buttonPingAll)$groupboxTools.Location = '308, 258'$groupboxTools.Name = 'groupboxTools'$groupboxTools.Size = '147, 163'$groupboxTools.TabIndex = 10$groupboxTools.TabStop = $False$groupboxTools.Text = 'Tools'$groupboxTools.UseCompatibleTextRendering = $True## buttonPing#$buttonPing.Location = '7, 44'$buttonPing.Name = 'buttonPing'$buttonPing.Size = '133, 23'$buttonPing.TabIndex = 12$buttonPing.Text = 'Ping'$buttonPing.UseCompatibleTextRendering = $True$buttonPing.UseVisualStyleBackColor = $True$buttonPing.add_Click($buttonPing_Click)## buttonВыход#$buttonВыход.Location = '7, 125'$buttonВыход.Name = 'buttonВыход'$buttonВыход.Size = '133, 23'$buttonВыход.TabIndex = 15$buttonВыход.Text = 'Выход'$buttonВыход.UseCompatibleTextRendering = $True$buttonВыход.UseVisualStyleBackColor = $True$buttonВыход.add_Click($buttonВыход_Click)## buttonWindox#$buttonWindox.Enabled = $False$buttonWindox.Location = '7, 17'$buttonWindox.Name = 'buttonWindox'$buttonWindox.Size = '133, 23'$buttonWindox.TabIndex = 11$buttonWindox.Text = 'Windox'$buttonWindox.UseCompatibleTextRendering = $True$buttonWindox.UseVisualStyleBackColor = $True$buttonWindox.add_Click($buttonWindox_Click)## buttonПеречитатьДанные#$buttonПеречитатьДанные.Location = '7, 98'$buttonПеречитатьДанные.Name = 'buttonПеречитатьДанные'$buttonПеречитатьДанные.Size = '133, 23'$buttonПеречитатьДанные.TabIndex = 14$buttonПеречитатьДанные.Text = 'Перечитать данные'$buttonПеречитатьДанные.UseCompatibleTextRendering = $True$buttonПеречитатьДанные.UseVisualStyleBackColor = $True$buttonПеречитатьДанные.add_Click($buttonПеречитатьДанные_Click)## buttonPingAll#$buttonPingAll.Location = '7, 71'$buttonPingAll.Name = 'buttonPingAll'$buttonPingAll.Size = '133, 23'$buttonPingAll.TabIndex = 13$buttonPingAll.Text = 'Ping All'$buttonPingAll.UseCompatibleTextRendering = $True$buttonPingAll.UseVisualStyleBackColor = $True$buttonPingAll.add_Click($buttonPingAll_Click)## groupboxRDP#$groupboxRDP.Controls.Add($comboboxRDP)$groupboxRDP.Controls.Add($textboxRDPLogin)$groupboxRDP.Controls.Add($textboxRdpPwd)$groupboxRDP.Controls.Add($buttonПодключитьRDP)$groupboxRDP.Location = '308, 128'$groupboxRDP.Name = 'groupboxRDP'$groupboxRDP.Size = '147, 126'$groupboxRDP.TabIndex = 5$groupboxRDP.TabStop = $False$groupboxRDP.Text = 'RDP'$groupboxRDP.UseCompatibleTextRendering = $True## comboboxRDP#$comboboxRDP.FormattingEnabled = $True$comboboxRDP.Location = '7, 17'$comboboxRDP.Name = 'comboboxRDP'$comboboxRDP.Size = '133, 21'$comboboxRDP.TabIndex = 6$comboboxRDP.Text = 'IP RDP сервера'## textboxRDPLogin#$textboxRDPLogin.Location = '7, 44'$textboxRDPLogin.Name = 'textboxRDPLogin'$textboxRDPLogin.Size = '133, 20'$textboxRDPLogin.TabIndex = 7$textboxRDPLogin.Text = 'RDP-login'## textboxRdpPwd#$textboxRdpPwd.Location = '7, 69'$textboxRdpPwd.Name = 'textboxRdpPwd'$textboxRdpPwd.PasswordChar = '*'$textboxRdpPwd.Size = '133, 20'$textboxRdpPwd.TabIndex = 8$textboxRdpPwd.Text = 'RDP-Password'## buttonПодключитьRDP#$buttonПодключитьRDP.Enabled = $False$buttonПодключитьRDP.Location = '7, 94'$buttonПодключитьRDP.Name = 'buttonПодключитьRDP'$buttonПодключитьRDP.Size = '133, 20'$buttonПодключитьRDP.TabIndex = 9$buttonПодключитьRDP.Text = 'Подключить RDP'$buttonПодключитьRDP.UseCompatibleTextRendering = $True$buttonПодключитьRDP.UseVisualStyleBackColor = $True$buttonПодключитьRDP.add_Click($buttonПодключитьRDP_Click)## groupboxVPN#$groupboxVPN.Controls.Add($buttonПодключитьVPN)$groupboxVPN.Controls.Add($buttonОтключитьVPN)$groupboxVPN.Controls.Add($checkboxШлюзПоумолчанию)$groupboxVPN.Location = '308, 27'$groupboxVPN.Name = 'groupboxVPN'$groupboxVPN.Size = '147, 98'$groupboxVPN.TabIndex = 1$groupboxVPN.TabStop = $False$groupboxVPN.Text = 'VPN'$groupboxVPN.UseCompatibleTextRendering = $True## buttonПодключитьVPN#$buttonПодключитьVPN.Enabled = $False$buttonПодключитьVPN.Location = '7, 45'$buttonПодключитьVPN.Name = 'buttonПодключитьVPN'$buttonПодключитьVPN.Size = '133, 20'$buttonПодключитьVPN.TabIndex = 3$buttonПодключитьVPN.Text = 'Подключить VPN'$buttonПодключитьVPN.UseCompatibleTextRendering = $True$buttonПодключитьVPN.UseVisualStyleBackColor = $True$buttonПодключитьVPN.add_Click($buttonПодключитьVPN_Click)## buttonОтключитьVPN#$buttonОтключитьVPN.Enabled = $False$buttonОтключитьVPN.Location = '7, 67'$buttonОтключитьVPN.Name = 'buttonОтключитьVPN'$buttonОтключитьVPN.Size = '133, 20'$buttonОтключитьVPN.TabIndex = 4$buttonОтключитьVPN.Text = 'Отключить VPN'$buttonОтключитьVPN.UseCompatibleTextRendering = $True$buttonОтключитьVPN.UseVisualStyleBackColor = $True$buttonОтключитьVPN.Visible = $False$buttonОтключитьVPN.add_Click($buttonОтключитьVPN_Click)## checkboxШлюзПоумолчанию#$checkboxШлюзПоумолчанию.Location = '7, 19'$checkboxШлюзПоумолчанию.Name = 'checkboxШлюзПоумолчанию'$checkboxШлюзПоумолчанию.Size = '133, 24'$checkboxШлюзПоумолчанию.TabIndex = 2$checkboxШлюзПоумолчанию.Text = 'Шлюз по-умолчанию'$checkboxШлюзПоумолчанию.TextAlign = 'MiddleRight'$checkboxШлюзПоумолчанию.UseCompatibleTextRendering = $True$checkboxШлюзПоумолчанию.UseVisualStyleBackColor = $True## richtextboxinfo#$richtextboxinfo.Cursor = 'Default'$richtextboxinfo.ForeColor = 'WindowText'$richtextboxinfo.HideSelection = $False$richtextboxinfo.Location = '461, 27'$richtextboxinfo.Name = 'richtextboxinfo'$richtextboxinfo.ReadOnly = $True$richtextboxinfo.ScrollBars = 'ForcedVertical'$richtextboxinfo.ShowSelectionMargin = $True$richtextboxinfo.Size = '290, 394'$richtextboxinfo.TabIndex = 16$richtextboxinfo.Text = ''$richtextboxinfo.add_LinkClicked($richtextboxinfo_LinkClicked)## listbox_clients#$listbox_clients.FormattingEnabled = $True$listbox_clients.Location = '12, 27'$listbox_clients.Name = 'listbox_clients'$listbox_clients.Size = '290, 394'$listbox_clients.TabIndex = 0$listbox_clients.add_SelectedIndexChanged($listbox_clients_SelectedIndexChanged)#Save the initial state of the form$InitialFormWindowState = $formКлиентыАльбус.WindowState#Init the OnLoad event to correct the initial state of the form$formКлиентыАльбус.add_Load($Form_StateCorrection_Load)#Clean up the control events$formКлиентыАльбус.add_FormClosed($Form_Cleanup_FormClosed)#Store the control values when form is closing$formКлиентыАльбус.add_Closing($Form_StoreValues_Closing)#Show the Formreturn $formКлиентыАльбус.ShowDialog()}#Запуск приложения!Main ($CommandLine) 

Скрипт можно запускать как скрипт ps1 или скомпилировать в exe через ps2exe и использовать как полноценное приложение

Подробнее..

Геопространственное моделирование с применением методов машинного обучения

18.06.2021 18:20:53 | Автор: admin


Всем привет! Меня зовут Константин Измайлов, я руководитель направления Data Science в Delivery Club. Мы работаем над многочисленными интересными и сложными задачами: от формирования классических аналитических отчетов до построения рекомендательных моделей в ленте приложения.

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

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

Статья написана по мотивам выступления с Евгением Макиным на конференции Highload++ Весна 2021. Для тех, кто любит видео, ищите его в конце статьи.

Бизнес-модель работы Delivery Club


Бизнес-модель Delivery Club состоит из двух частей:

  • ДДК (доставка Деливери Клаб): мы передаем заказ в ресторан и доставляем его клиенту, то есть ресторану остается только приготовить заказ к определенному времени, когда придет курьер.
  • МП (маркетплейс): мы передаем заказ в ресторан, а он своими силами доставляет заказ в пределах своей согласованной зоны.

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

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

Рисуем зону доставки ресторана


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



Как процесс выглядел раньше


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

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



Стоит упомянуть и про SLA (Service Level Agreement соглашение о максимальной длительности отрисовки зоны доставки для одного партнера): онбординг партнера или подготовка его зоны для внедрения в нашу платформу составляли порядка 40 минут для одного заведения. Представьте, что к вам подключилась городская сеть с сотней ресторанов, а если это ещё и жаркий сезон, например, после проведения рекламной акции Вот наглядное доказательство неэффективности ручной отрисовки:

$T = R * SLA = 100 * 40\ минут =\ \sim 67\ часов\ ручной\ работы$


где $T$ время, которое будет затрачено на отрисовку всех зон доставки партнера,
$R$ количество ресторанов,
$SLA$ время на отрисовку одной зоны.

Проблемы ручной отрисовки зон:


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

Baseline


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

При этом оставались недостатки:

  • артефакты в зонах доставки (стандартный случай с переходом через реку);
  • единообразный подход к партнерам (не учитываются индивидуальные KPI партнеров).

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



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

Преимущества технологии H3


Мы стремились прийти к универсальному решению. Каких-то простых и популярных подходов найти не удалось, поэтому мы сосредоточились на разработке собственного алгоритма. А за основу взяли технологию H3 компании Uber.


Источник: eng.uber.com/h3

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

Также стоит отметить, что:

  1. Существует хорошая библиотека для работы с H3, которую и выбрала наша команда в качестве основного инструмента. Библиотека поддерживает многие языки программирования (Python, Go и другие), в которых уже реализованы основные функции для работы с гексагонами.
  2. Наша реляционная аналитическая база Postgres поддерживает работу с нативными функциями H3.
  3. При использовании гексагональной сетки благодаря ряду алгоритмов, работающих с индексами, можно очень быстро получить точную информацию о признаках в соседних ячейках, например, определить вхождение точки в гексагон.
  4. Преимуществом гексагонов является возможность хранить информацию о признаках не только в конкретных точках, но и в целых областях.

Алгоритм построения зон доставки


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


  2. Убираем точки-выбросы. Анализируем все постройки и сразу отбрасываем те, которые нас не интересуют. Например, какие-то мелкие нежилые объекты. Далее с помощью DBSCAN формируем кластеры точек и отбрасываем те, которые для нас не являются важными: например, если кластер находится далеко от ресторана или нам экономически невыгодно доставлять туда.


  3. Далее на основе очищенного набора точек применяем триангуляцию Делоне.


  4. Создаем сетку гексагонов H3.


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


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



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

    • минимизацию времени доставки при фиксированном покрытии;
    • максимизацию охвата пользователей при фиксированном времени доставки.

    Пример функции ошибки для минимизации времени доставки:

    ${L_{min}}_{time}\;=\;min(\sum_{i=1}^n\;({t_{rest}}_i)/n),$


    где $L_{min_ {time}}$ функция ошибки минимизации времени доставки с фиксированным покрытием,
    $t_{rest_ {i}}$ время от ресторана i до клиента,
    $n$ количество клиентов в зоне доставки.

  7. Далее строим временной градиент в получившихся зонах (с очищенными выбросами) и с заранее определенными интервалами (например, по 10-минутным отрезкам пешего пути).



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

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

Внедрение


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

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

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

Но тут пришел COVID-19

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

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

Оценка


После решения всех горящих проблем нам нужно было немного отдышаться и понять, что мы вообще наделали. Для этого воспользовались A/B-тестом, а точнее его вариацией switch-back. Мы сравнивали зоны ресторанов с одинаковыми входными параметрами, оценивали GMV и время доставки, где в качестве контроля у нас были простые автоматически отрисованные зоны в виде окружностей и прямоугольников, либо зоны, отрисованные операторами вручную.



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

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

$T = 100 * 3,6\ секунды =\ \sim 6\ минут$


Ускорение в 670 раз!

Текущая ситуация и планы


Сервис работает в production. Зоны автоматически строятся по кнопке. Появился более гибкий инструмент для работы со стоимостью доставки для клиентов в зависимости от их удаленности от ресторана. 99,9% ресторанов (изредка ещё нужно ручное вмешательство) перешли на алгоритмические зоны, а наш алгоритм поспособствовал переходу бэкенда на H3.

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

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

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

Всем спасибо!

Подробнее..

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

11.06.2021 16:11:37 | Автор: admin

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

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


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

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

Машина

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

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

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

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

Теория

Повышенную точность модели случайного леса можно назвать коллективным интеллектом. Этот термин вошёл в обиход в 1906 году после того, как на ярмарке скота в Плимуте, штат Массачусетс, провели конкурс на угадывание веса быка. Угадать вес пытались почти 800 фермеров. Позже статистик сэр Фрэнсис Гальтон проанализировал все оценки и пришёл к выводу, что, несмотря на то что отдельные оценки сильно отличались друг от друга, среднее значение оценок было более точным, чем любая отдельно взятая оценка. Гальтон изложил свою теорию в знаменитом труде Vox Populi.

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

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

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

Рыбы

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

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

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

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

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

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

Второй класс алгоритмов базируется на предположении о том, что стаи рыб действуют как крупная нейронная сеть. (От биологических нейронов до искусственных нейронных сетей и стай рыб... мы прошли полный круг!) Уходя от хищников, многие виды рыб проявляют стартл-рефлекс сверхбыстрый рефлекторный рывок в сторону от тревожного раздражителя[2].

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

Каскад стартл-рефлексов. Из научной работы Розенталя и других учёных 2015 года: https://www.pnas.org/content/pnas/early/2015/03/24/1420068112.full.pdf?with-ds=yesКаскад стартл-рефлексов. Из научной работы Розенталя и других учёных 2015 года: https://www.pnas.org/content/pnas/early/2015/03/24/1420068112.full.pdf?with-ds=yes

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

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

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

Решения принимает нейронная сеть из нейронных сетейРешения принимает нейронная сеть из нейронных сетей

Заключение

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

Хотите узнать больше? Советуем почитать, как бабуины принимают демократические решения о передвижении, как у диких птиц из поколения в поколение сохраняется инновационное поведение [значение понятия можно прочитать здесь, этой ссылки в оригинальной статье нет], как слизистые грибы помогли заново создать карту токийского метро, оптимизировав распределение ресурсов. Чтобы узнать о последних исследованиях в области коллективного поведения, также рекомендуем ознакомиться с интернет-ресурсом Отдела коллективного поведения Института поведения животных им. Макса Планка.

Сноски

1. Теория
Если вы никогда не встречаете иные точки зрения в Интернете или такие иные точки зрения представлены как точки зрения идиота, то, скорее всего, вы находитесь в эхо-камере. Такое происходит довольно часто, поскольку социальные сети, как ни странно, имеют самоизолирующий эффект. Для более объективного взгляда на мир необходимо знакомиться с различными точками зрения.

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

Эта статья прекрасное напоминание о том, что многие решения в науке и технике позаимствованы у природы, либо просто существуют в ней уже очень давно. Если вы хотите экспериментировать с моделями машинного и глубокого обучения, повторяя и совершенствуя находки природы, находить новое в комбинациях разнообразных подходов к искусственному интеллекту, то вы можете обратить внимание на наш курс "Machine Learning и Deep Learning", партнёром которого является компания NVIDIA лидер в области вычислений для ИИ.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Анализ результатов работы архитектуры YoloV3 на медицинских снимках

31.05.2021 12:16:05 | Автор: admin

Данная статья представляет собой обзор на оригинальную статью на Medium (эксперименты проводятся с изменениями некоторых условий).

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

http://personeltest.ru/aways/github.com/ultralytics/yolov3https://github.com/ultralytics/yolov3

https://github.com/ultralytics/yolov3

Можно заметить, что прямоугольники подписаны какими-то словами и числами. На картинке это person и tie. Рядом с этими словами написаны числа (у человека слева это person с 0.59, tie - 0.62). Эти слова образуют виды объектов (например, машина, человек, кот, мяч и т.д.), которые нужно распознать, а числа, записанные рядом с этими словами, есть вероятность того, что данный объект принадлежит этому классу. (Опять же у человека справа, стоит "person 0.59". Это значит, что в выделенном прямоугольнике есть объект класса person - человек - с вероятностью 0.59). И да, число - вероятность объекта в данном прямоугольника, принимает значения от 0 до 1.

Задача

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

Пневмония является одной из распространенных болезней, которое представляет собой воспалительное заболевание легких. По официальным данным, именно она является одной из опасных инфекционных заболеваний за последние 20 лет. В 2019-м эти болезни оказались четвертой причиной смертности в мире (от них скончались 2,6 млн человек). Обычно пневмония проявляется в виде областей повышенной непрозрачности на снимках рентгенограммы. Однако диагностика рентгенограмм затруднена из-за ряда причин, связанных с состоянием легких. И иногда даже опытному специалисту бывает сложно поставить диагноз.

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

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

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

Модель

В качестве такой сети возьмем архитектуру YOLOv3. Почему именно она? Да, просто захотели =) Более подробно про эту архитектуру можно почитать на официальном сайте и Хабре.

YOLOv3 представляет собой нейронную сеть, основанную на архитектуре YOLO (You Only Look Once). Она примечательна тем, что CNN (Convolutional Neural Network) применяется один раз ко всему изображению сразу (отсюда и название). YOLOv3 состоит из 106-ти свёрточных слоев. Стоит отметить, что у YOLOv3 есть несколько слоев (их 3), которые предназначены для детекции объектов разного размера. На картинке ниже представлена архитектура YOLOv3:

http://personeltest.ru/aways/www.researchgate.net/figure/The-framework-of-YOLOv3-neural-network-for-ship-detection_fig2_335228064https://www.researchgate.net/figure/The-framework-of-YOLOv3-neural-network-for-ship-detection_fig2_335228064

https://www.researchgate.net/figure/The-framework-of-YOLOv3-neural-network-for-ship-detection_fig2_335228064

При использовании YOLO изображение делится на сетку с ячейками размером 13 х 13. Для чего нужны эти ячейки? Дело в том, что каждая такая ячейка прогнозирует количество bounding box'ов (или ограничивающих прямоугольников) и вероятность того, что в данной области находится некоторый объект. Эта вероятность (точнее, число) называется confidence value (доверительное значение). И получается, что если в некоторой области объекта нет, то его доверительное значение маленькое (точнее, этого мы хотим достичь). Ниже представлена схема работы YOLOv3.

http://personeltest.ru/aways/medium.com/nerd-for-tech/a-real-time-object-detection-model-using-yolov3-algorithm-for-non-gpu-computers-8941a20b445https://medium.com/nerd-for-tech/a-real-time-object-detection-model-using-yolov3-algorithm-for-non-gpu-computers-8941a20b445

https://medium.com/nerd-for-tech/a-real-time-object-detection-model-using-yolov3-algorithm-for-non-gpu-computers-8941a20b445

Также примечательно, что YOLO использует, так называемые anchor boxes (якорные рамки). Подробнее о них написано в статье на Medium. Это достаточно сложная для понимания(лично для автора этой статьи) концепция. Нам важно лишь то, что anchor boxes (якорные рамки) используются для прогнозирования bounding box'ов и рассчитаны они с помощью датасета COCO с использованием кластеризации k-средних.

Чтобы более подробно познакомиться с YOLOv3 подойдет вот эта статья.

Данные

С задачей определились, с моделью определились. Что еще надо? Правильно, данные. Данные берутся из платформы Kaggle, в которой проводились соревнования по детекции пневмонии. Вот данные.

Изучим эти данные более подробно. Нам понадобятся изображения из файлов stage_2_train_images.zip и stage_2_test_images.zip. Данные, которые давались на соревновании, представляют собой набор снимков рентгенограммы грудной клетки. В датасете (а именно так называются набор данных) содержатся 26684 рентгеновских снимков разных пациентов. Данные снимки представляют собой изображения в формате DICOM в разрешении 1024 х 1024. Пример изображения представлен ниже.

Class

Target

Patients

Lung Opacity

1

9555

No Lung Opacity / Not Normal

0

11821

Normal

0

8851

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

import pydicom as dicomimport osfrom tqdm import tqdmimport numpy as npimport cv2import pandas as pdперевод dicom в jpgdef dicom_to_jpg(source_folder,destination_folder,labels):    images_path = os.listdir(source_folder)    image_dirs_label = {'image_dir':[],'Target':[]}    for n, image in tqdm(enumerate(images_path)):        ds = dicom.dcmread(os.path.join(source_folder, image))        pixel_array_numpy = ds.pixel_array        image = image.replace('.dcm', '.jpg')        cv2.imwrite(os.path.join(destination_folder, image), pixel_array_numpy)        image_dirs_label['image_dir'].append(os.path.join(destination_folder, image))        image_dirs_label['Target'].append(train_labels[train_labels.patientId== image.split('.')[0]].Target.values[0])    print('{} dicom files converted to jpg!'.format(len(images_path)))    return pd.DataFrame(image_dirs_label)

Выделяются 3 класса, которые представляют для интерес: Normal 0, No Lung Opacity / Not Normal 0, Lung Opacity 1. Классы Class, целевые признаки Target и количество изображений Patients, соответствующего класса, представлены в таблице выше. И картинка ниже показывает изображения каждого класса.

Для нас особый интерес представляют классы, сигнализирующие пневмонию (positive или на картинке выше Lung Opacity). И соотношение этого класса к классу изображений здоровых пациентов (negative) равно примерно 1:4 (ниже есть диаграмма, иллюстрирующая данное соотгношение).

Дисбаланс классовДисбаланс классов

Дисбаланс классов

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

import albumentations as Aimport pandas as pdimport cv2import ostransformertransform = A.Compose([        A.RandomRotate90(),        A.Flip(),        A.Transpose(),        A.OneOf([            A.IAAAdditiveGaussianNoise(),            A.GaussNoise(),        ], p=0.2),        A.OneOf([            A.MotionBlur(p=.2),            A.MedianBlur(blur_limit=3, p=0.1),            A.Blur(blur_limit=3, p=0.1),        ], p=0.2),        A.ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.2, rotate_limit=45, p=0.2),        A.OneOf([            A.OpticalDistortion(p=0.3),            A.GridDistortion(p=.1),            A.IAAPiecewiseAffine(p=0.3),        ], p=0.2),        A.OneOf([            A.CLAHE(clip_limit=2),            A.IAASharpen(),            A.IAAEmboss(),            A.RandomBrightnessContrast(),        ], p=0.3),        A.HueSaturationValue(p=0.3),    ])

Реализация модели

Данный раздел будет обновляться в будущем(ибо есть технические "подводные камни", о которых не рассказано, но о которых стоит рассказать)

Теперь датасет мы "расширили". Преобразованные изображения и исходные файлы в форматах JPG и DICOM будем анализировать с использованием архитектуры YOLOv3 с основой (backbone'ом) DarkNet. Подробнее про DarkNet можно почитать здесь. Затем основа архитектуры YOLOv3 (в данном случае Darknet) заменяется на обученную классификационную модель CheXNet. CheXNet представляет собой 121-слойную свёрточую нейронную сеть, которая определяет области легких, сигнализирующих о пневмонии. Рекомендуется прочитать эту научную работу про CheXNet. Эта модель обучена на классификацию 14 классов, поэтому так как мы решаем задачу бинарной классификации, то последние слои CheXNet необходимо установить на классификацию 2-х классов (negative пневмонии нет и positive пневмония есть). И реализовать в коде данную модель можно с помощью библиотеки TensorFlow, в которой есть готовая заготовка DenseNet121. Реализация этой модели представлено ниже.

# Для CheXNet устанавлиются веса classifier_weights.hdf5, которые можно скачать отсюдаhttps://drive.google.com/file/d/1Bd50DpRWorGMDuEZ3-VHgndpJZwUGTAr/viewfrom absl import flagsfrom absl.flags import FLAGSimport numpy as npimport tensorflow as tffrom tensorflow.keras import Modelfrom tensorflow.keras.applications import DenseNet121from tensorflow.keras.layers import (    Add,    Concatenate,    Conv2D,    Input,    Lambda,    LeakyReLU,    MaxPool2D,    UpSampling2D,    ZeroPadding2D,    BatchNormalization,    Dense)def base_model(chexnet_weights=None,size=None):    dense_net_121 = DenseNet121(input_shape = [size,size,3], include_top = False,pooling = 'avg')    base_model_output = Dense(units = 14, activation = 'relu')(dense_net_121.output)    base_model = Model(inputs = dense_net_121.input,outputs = base_model_output)    output_layer = Dense(1, activation = 'sigmoid')(base_model.layers[-2].output)    model = Model(inputs = base_model.inputs, outputs = output_layer)    if chexnet_weights:        model.load_weights(chexnet_weights)    final_base_model = Model(inputs = model.inputs, outputs = model.layers[-3].output)    return final_base_modeldef ChexNet(name=None, chexnet_weights='PATH_TO_WEIGTHS/classifier_weights.hdf5',size=None):    chexnet = base_model(chexnet_weights = chexnet_weights, size = size)    back_bone = Model(inputs = chexnet.inputs, outputs=(chexnet.get_layer('pool3_conv').output,                                                           chexnet.get_layer('pool4_conv').output,                                                           chexnet.output),name=name)    return back_bone

Теперь посмотрим на количество параметров каждой модели:

Model

Total params

Trainable params

Non-trainable params

DarkNet

61576342

61523734

52608

CheXNet

27993206

27892662

100544

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

Обучение

Полученная архитектура нейронной сети YOLOv3 с основой CheXNet обучается на преобразованных данных(над которыми был совершен процесс аугментации).

Стоит отметить то, что мы сначала обучаем (1 эпоху) на всех классах изображений (positive и negative), а затем на изображениях, в которых есть пневмония (класса positive). Это делается потому что в YOLOv3 изображение 416 х 416 делится на сетку 13 х 13 (416 / 32 = 13). И прогноз делается для каждой ячейки сетки 13 х 13. И если количество anchor box'ов равно 3, тогда каждая такая ячейка сетки 13 х 13 связана с 3-мя anchor box'ами. То есть размерность будет 13 х 13 х 3 = 507 (всего будет столько предсказаний). Получается, что для одного изображения мы делаем 507 предсказаний. И даже если изображение относится к классу positive (пневмония есть) и в нем есть 2 области непрозрачности (помутнения), то будет 2 положительных предсказания и 507-2=505 отрицательных предсказаний. Как видно, число отрицательных предсказаний намного больше. Поэтому если мы снова добавим отрицательные изображения, это сделает нашу модель "предвзятой" по отношению к отрицательному классу.

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

# true_augmented_labels - это DataFrame, который содержит информацию о всех изображениях (и о изначальных, и аугментированных(преобразованных)datagen=ImageDataGenerator(        rescale = 1. / 255.,        validation_split = 0.20)train_generator = datagen.flow_from_dataframe(dataframe = true_augmented_labels,x_col = "image_dir",y_col = "Target",subset = "training",batch_size = 4,seed = 42,shuffle = True,class_mode = "binary",target_size = (416, 416))valid_generator = datagen.flow_from_dataframe(dataframe = true_augmented_labels,x_col = "image_dir",y_col = "Target",subset = "validation",batch_size = 4,seed = 42,shuffle = True,class_mode = "binary",target_size = (416, 416))

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

# веса brucechou1983_CheXNet_Keras_0.3.0_weights.h5 и classifier_weights.hdf5можно скачать отсюда https://www.kaggle.com/theewok/chexnet-keras-weights/version/1и отсюда https://github.com/junaidnasirkhan/Replacing-YoloV3-Backbone-with-ChexNet-for-Pneumonia-Detectiondense_net_121 = DenseNet121(input_shape = [416,416] + [3], include_top = False, pooling = 'avg')base_model_output = Dense(units = 14, activation = 'relu')(dense_net_121.output)base_model = Model(inputs = dense_net_121.input, outputs = base_model_output)загрузка "тренированных" весовbase_model.load_weights('brucechou1983_CheXNet_Keras_0.3.0_weights.h5')заморозка последних слоев моделиfor layer in base_model.layers[:10]:    layer.trainable = Falseустанавлием последние слои модели на бинарную классификациюoutput_layer = Dense(1, activation = 'sigmoid')(base_model.layers[-2].output)model = Model(inputs = base_model.inputs, outputs = output_layer)model.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy', f1_m]) checkpoint = ModelCheckpoint(filepath = 'classifier_weights.hdf5', monitor = 'val_accuracy',  verbose = 0, save_best_only = True, save_weights_only = True, mode = 'auto')log_dir = "classifier_logs/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")tensorboard = TensorBoard(log_dir = log_dir, histogram_freq = 1, write_graph = True, write_grads = True)callback_list = [checkpoint, tensorboard]обучаем модельmodel.fit(train_generator,  validation_data = valid_generator,  epochs = 1, # в оригинальной статье стоит 3  steps_per_epoch = len(train_generator),  callbacks = callback_list)

Затем нам надо написать функцию обучения на positive изображениях (причина описана выше). Она представлена ниже

# Для обучения модели были созданы файлы rsna_train_pos.tfrecord и rsna_val_pos.tfrecordКлассы изображений записываются в формате .names (в нашем случае)это классы "opacity" и "no_opacity"model = train(dataset = 'PATH_TO_TFRECORD/rsna_train_pos.tfrecord',          val_dataset = 'PATH_TO_TFRECORD/rsna_val_pos.tfrecord',          backbone = 'chexnet',          classes = 'PATH_TO_CLASSES/RSNA_VOC.names',           size = 416,          epochs = 30,          batch_size = 16,          learning_rate = 1e-4,          num_classes = 1)

После обучения веса модели бинарной классификации сохраняются в виде файла формата hdf5.

Результаты обучения

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

С параметрами learning_rate = 1e-4, epoch = 20

Посмотрим на loss'ы

Аналогично для learning_rate = 1e-4, epochs = 30

Посмотрим на loss'ы

Выводы

  • Над исходными данными был совершен процесс аугментации с целью увеличения количества данных.

  • Анализ количество обучаемых параметров моделей CheXNet и DarkNet показал, что таких параметров меньше у модели CheXNet, что делает ее обучение быстрым по сравнению с обучением модели DarkNet.

  • Архитектура с классификационной моделью CheXNet была обучена 1 эпоху на изображениях всех классов, а затем 20 эпох и 30 эпох на изображениях, содержащих признаки пневмонии.

  • Эксперименты показали, что с увеличением числа epoch, растет и точность предсказаний модели.

Перспективы

Рассматривается возможность улучшения показателей архитектуры. Этого можно достичь путем:

  • обучения с изменением параметров (увеличения количества эпох, значения learning_rate)

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

  • модель CheXNet можно заменить другую классификационную модель

Ссылки

Подробнее..

Инструменты для алготрейдинга на Python. SMA Полосы Боллинджера на акциях Северстали код готовой стратегии

31.05.2021 18:12:13 | Автор: admin

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

Предыдущая статья о "Расчете дневного изменения цены"

Когда я писал прошлую статью (она была первой из цикла) я не предполагал, что читатели разделятся на 2 категории:
1. Те, кто верят, что в алготрейдинг
2. Те, кто верят, что я шарлатан

Для обоих групп я напоминаю, что цель алготрейдинга - это увеличить вероятность получить прибыль от сделки
Или же, как говорят в "теории игр" - сделать математическое ожидание от игры положительным

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

Собственно, здесь я перехожу к сути данной статьи.

SMA (Simple Moving Average, Скользящее среднее) - индикатор, основанный на подсчете среднего значения цены закрытия ценной бумаги.

Для тех, кто не знает что такое SMA, приведу алгоритм его подсчета:
1. Взять цену закрытия "close" ценной бумаги за период от t1 до t2 и отсортировать ее от t1 к t2.
2. Взять таймфрейм из первых N значений цены close.
3. Посчитать среднее арифметическое значение таймфрейма (simple average).
4. Сдвинуть таймфрейм вперед на одно значение (происходит moving) и выполнить пункт 3
5. Пункт 4 проводить до тех пор, пока таймфрейм не дойдет до точки t2

Отрисуем график SMA (N=20) для цены close акций Северсталь (тикер CHMF) за 27 мая 2021г.:

По графику видно, что SMA является сглаженной версией цены Close с временным лагом в 20 периодов.

Полосы Боллинджера (Bollinger Bands)

В 1980х годах Джон Боллинджер предложил рассчитывать не только SMA, но и STD (standart deviation, среднеквадратическое отклонение). Таким образом, мы будем видеть не только график изменения средней цены, но и ее волатильность.

Обычно, значения std устанавливают равным 2. В таком случае, с вероятностью в 95% следующее значение цены close будет лежать внутри полосы Боллинджера и только в 5% случаях оно будет выходить из этой полосы.

В тех местах, где цена close близка к нижней грани полосы Боллинджера, стоимость акций считается низкой. И, наоборот, если цена close близка к верхней грани полосы Боллинджера, стоимость акций считается высокой.

И тут у трейдера срабатывает чуйка: покупаем на низах, продаем на хаях (никак не наоборот).

Весь код с использованием полос Боллинджера привел на Google Colab. Данная стратегия принесла +1,7% за 1 день (но это не точно).

В следующей статье поговорим об RSI

Подробнее..

Альтернативный способ заполнения спиральной матрицы

31.05.2021 18:12:13 | Автор: admin

В процессе изучения основ алгоритмизации и программирования в качестве студента еще в середине 2000х мне попалась довольно известная всем задача по заполнению спиральной матрицы. Суть состоит в том, чтобы начиная с позиции [1, 1], продвигаясь по часовой стрелке, заполнить квадратную матрицу заданной величины числами в возрастающем порядке. На ее решение было потрачено около двух часов.

Собственно алгоритм заполнения был тривиален, циклы, в общей сложности состоящие из N2 итерации, предполагали прохождение по всем элементам массива в требуемом порядке, при этом увеличивая значение итератора на 1 и заполняя им текущий элемент матрицы. Маршрут начинался с элемента [1, 1], далее продвигается по горизонтали до правого верхнего элемента [1, N], после вниз до нижнего правого угла [N, N], затем до левого нижнего угла [N, 1] и завершал первый круг на столбец ниже отправной точки [2, 1]. В дальнейшем, такое же круговое движение происходило уже в следующем внутреннем круге, и так далее до центра матрицы.

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

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

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

Спустя эти самые 18 лет, перебирая наиболее интересные задачи и пути их решения для обучения уже следующего поколения представителей неординарной профессии программист, меня заинтересовала одна статья на ресурсе Хабр (http://personeltest.ru/aways/habr.com/ru/post/261773), описывающая процесс создания формулы для вычисления количества дней в заданном месяце без использования каких-либо условных операторов, циклов и заранее подготовленных данных.

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

Именно после этого я вспомнил ту самую спиральную матрицу и решился на очередную попытку выведения универсальной формулы.

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

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

Условие: никаких условных (прошу прощения за тавтологию) переходов или заготовленных данных в виде массивов, словарей и т.д.

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

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

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

2) Найти связь между, опять же, координатами ячейки и номером кольца, в котором она находится. Составить формулу.

3) Связать между собой два алгоритма для выведения общей формулы вычисления значения элемента массива.

Входные данные: N размер квадратной матрицы (массива).

1 этап. Заполнение внешнего кольца. Для начала попытаемся вывести формулу вычисления значения ячейки для внешнего кольца массива, в котором отсчет начинается с ячейки [1, 1] и двигается по часовой стрелке поворачивая на углах [1, N], [N, N]. Заканчивается на строку ниже начальной точки, т.е. [2, 1].

Математический аппарат будет разрабатываться параллельно с написанием кода на языке программирования, в качестве которого выбираем C++, как наиболее распространенный и удобный язык программирования (здесь конечно читатели могут поспорить, но классика есть классика). Но, в принципе, выведенная формула не должна иметь привязки языке.

Для наглядного изучения процесса напишем соответствующий скрипт, объявив в нем массив размером 5x5 (назовем его a), элементы которого будем перебирать традиционным способом двумя вложенными циклами от 1 до N. На данном этапе будем работать только с внешним кольцом.

#include <iostream>using namespace std;int main(){    int N = 5;          // задаем размер матрицы    int a[N][N];        // и инициализируем ее    for (int ik = 0; ik < N; ik++)        for (int jk = 0; jk < N; jk++)            a[ik][jk] = 0;          // заполнив для удобства нулями                                          for (int ik = 0; ik < N; ik++){     //назовем его "Основной цикл"        for (int jk = 0; jk < N; jk++){            if (!(ik == 0 || ik == N - 1 || jk == 0 || jk == N - 1))                 continue;      // Временное условие для фильтрации элементов внесшего "кольца"            int i = ik + 1;     // Номера строк и столбцов приводим в удобный            int j = jk + 1;     // в математическом плане вид (от 1 до N)              //  ... здесь будем вставлять основной код вычислений        }       }        for (int ik = 0; ik < N; ik++){          //Блок "Вывод массива"        for (int jk = 0; jk < N; jk++){           printf( "%02d ", a[ik][jk]);// дополняем число ведущими нулями        }        cout << endl;    }      return 0;}

Очевидно, что, по крайней мере, до правого нижнего угла внешнего кольца суммарное значение координат (i + j) увеличивается ровно на 1 с каждым шагом. Однако первый элемент в таком случае равняется 2, E1,2 = 3 и т.д. Поэтому необходимо уменьшить значение суммы (i + j) на один. В результате введем переменную Xs = i + j - 1, которая часто будет использоваться в дальнейших вычислениях. Пишем код:

int Xs = (i + j  1);a[ik][jk] = Xs;

В результате запуска первого скрипта получаем массив:

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

Очевидно a[ik][jk] = Xsнуждается в дополнении, при котором все его значения до [5, 5] останутся неизменными, но после этой точки начнут увеличиваться на 1.

Но для начала постараемся привести в норму вторую часть кольца, которая заполнялась бы с ячейки (i = 5, j = 4) начиная со значения 10. В данном случае это легко сделать, лишь вычитая от общего количества элементов первого кольца увеличенного на два (равняется периметру первого кольца N * 4 - 4 = 16 плюс 2) значение Xs.

То есть a1,2 = 4N 4 + 2 Xs = 4N Xs - 2.

int Xs = i + j - 1;     a[ik][jk] = 4 * N - Xs - 2;

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

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

1. ai,j = Xs = i + j - 1; действует от [1, 1] до [N, N].

2. ai,j = 4*N 2 - X; действует от [N, N] до [2, 1].

Самым простым решением было бы использование условного перехода, однако это не соответствует нашей начальной установке. Здесь необходимо дополнительно отметить, что из всех стандартных кусочно-заданных функций, как отмечено выше, мы будем использовать только модуль (y = |x|) и формулы собственной разработки.

В этой связи, необходимо привести наше уравнение в вид:

a_1, _2= F _1(switcher) * Xs + F _2 (switcher) * (4 * N Xs - 2);

Здесь функция F1 принимает значение 1 при i, j между [1, 1] [N, N] , в остальных случаях 0. В свою очередь, F2 , наоборот, принимает значение 1 когда ячейка находится в диапазоне [N, N - 1] [2, 1], и 0 между [1, 1] [N, N].

В качестве аргумента функции используется переменная switcher, вычисляемая на основе значении i и j, и опять же, принимающая различные значения в вышеуказанных диапазонах.

Неплохим вариантом выглядит идея получения значений -1, 0 и/или 1 от манипуляций с координатами элемента. Тогда F1 и F2 содержали бы простые арифметические операции.

Итак, мы уже использовали сложение i и j, но оно всегда дает положительное число. Почему бы сейчас не попробовать вычитание?

Заменим в нашем предыдущем листинге значение a[ik][jk].

int Xs = i + j - 1;     a[ik][jk] = j  i;

Наиболее легким вариантом видится целочисленное деление на самого себя, но в таком случае мы столкнемся с ошибкой деления на 0, поскольку а[1][1] и а[N][N] уже содержат 0. В данном случае можно прибавить ко всем значениям N и осуществить целочисленное деление на N. Введенную переменную назовем switcher.

int switcher =  (j - i + N) / N;a[ik][jk] =  switcher; // временно подставим в ячейки switcher

Теперь осталось немногое для завершения данного этапа, а именно создать функции F1 и F2. F1 должна возвращать такое же значение, какое ей передали в качестве аргумента, т.е. F1 (switcher) = switcher. В таком случае F1 (switcher) * Xs работает только для диапазона от [1, 1] до [N, N], в остальных случаях равняется нулю. Вторая часть уравнения, должна действовать наоборот. В таком случае она должна возвращать значение по модулю switcher 1, т.е. F2.(switcher) = |switcher 1|.

Итак, пишем:

a[ik][jk] =  switcher * Xs + abs(switcher - 1) * (4 * N - 2 - Xs);

Проверяем:

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

2 этап. Альтернативная система координат.

Нам удалось заполнить внешнее первое кольцо требуемыми данными. Однако, что произойдет, если мы снимем фильтр, и попытаемся вычислить данные для остальных элементов массива? Для этого необходимо удалить участок кода if (!(ik == 0 || ik == N - 1 || jk == 0 || jk == N - 1)) continue;

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

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

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

Мы же, перенесем начало координат в центр нашей матрицы следующим образом:

a[ik][jk] =  abs(N / 2 + 1 -  i);

Поскольку нам требуется только расстояние ячейки от центра, мы берем только абсолютные значения.

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

Введем две новые переменные Ic, Jc (c обозначает center).

Ic = abs(i -  N / 2  - 1);Jc = abs(j -  N / 2  - 1);

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

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

a[ik][jk] =  Ic + Jc;

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

a[ik][jk] =  Ic - Jc;

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

a[ik][jk] =  abs(Ic  Jc) + Ic + Jc;

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

Ring = N / 2 -  (abs(Ic  Jc) + Ic + Jc) / 2; a[ik][jk] =  Ring;

Замечательно. Однако, при размере матрицы N = 6 данная формула работает не совсем корректно, так она в качестве центра считает только один элемент (что является справедливым для матриц с нечетной размерностью, как в предыдущих примерах).

N = 6

При четном размере центральный квадрат из четырех элементов должен считаться центром матрицы. Возникает вопрос: как это реализовать? Здесь к нам опять на помощь приходит целочисленное деление и кусочно-заданная функция.

Но для этого вернемся немного назад, к вычислению Ic и Jc. Попробуем запустить наш скрипт при N = 6, и посмотрим значения Ic = |i - N / 2 - 1|.

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

Ic = abs(i - N / 2  - 1) + (i - 1) / (N /2) * ((N-1) % 2);

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

Тоже самое проделываем и Jc.

Jc = abs(j - N / 2  - 1) + (j - 1)/(N /2) * ((N-1) % 2);

В итоге получаем симметричную матрицу уже по вертикали. Теперь проверяем значение Ring для матрицы с размером 6.

 a[ik][jk] =  Ring;

Все работает нормально. Второй этап завершен.

3 этап. Соединение. На данном этапе мы объединим полученные данные и выведем искомую матрицу (через формулу вычисления значения заданной ячейки).

Но для начала, вернемся к первому этапу и выведем на экран:

a[ik][jk] =  switcher * Xs + abs(switcher - 1) * (4 * N - 2 - Xs);

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

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

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

Xs = (i Ring) + (j Ring) 1.

Теперь попробуем вывести

a[ik][jk] =  switcher * Xs + abs(switcher - 1) * (4 * N - 2 - Xs);

Как вы могли заметить верхние и правые элементы внутренних колец пришли в требуемое значение. Однако нижние и левые стороны приняли гораздо большие значения, чем ожидалось. Это связано с тем, что выражение 4 * N - 2 - Xs во второй части функции вычисляет значения исходя из размера внешнего кольца, которое нужно уменьшить, заменив на N 2 * Ring. То есть формула будет работать в соответствии с размером текущего кольца.

Итак:

a[ik][jk] =  switcher * (Xs) + abs(switcher - 1) * (4 * (N - 2 * Ring) - 2 - Xs); 

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

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

Coef = N2 (N 2Ring)2

Воспользовавшись правилами разложения квадратов разницы элементов ((ab)2=a22ab+b2), можно сократить до 4Ring(N - Ring).

Теперь этот коэффициент нужно добавить к нашей основной формуле.

a[ik][jk] =  Coef + switcher * (Xs) + abs(switcher - 1) * (4 * (N - 2 * Ring) - 2 - Xs);

Требуемый результат!

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

int switcher =  (j - i + N) / N;int Ic = abs(i - N / 2  - 1) + (i - 1)/(N /2) * ((N-1) % 2);int Jc = abs(j - N / 2  - 1) + (j - 1)/(N /2) * ((N-1) % 2);int Ring = N / 2 - (abs(Ic - Jc) + Ic + Jc) / 2;int Xs = i - Ring + j - Ring - 1;    int Coef =  4 * Ring * (N - Ring);a[ik][jk] =  Coef + switcher * Xs + abs(switcher - 1) * (4 * (N - 2 * Ring) - 2 - Xs);

Можно конечно попробовать развернуть единую формулу, заменив дополнительные переменные выражениями, основанными только на i, j и N, и попытаться сократить ее математическими методами. Но поверьте мне, я пытался, получилось нечто такое невообразимое (на половину страницы), что я решил оставить все как есть.

(PS. Не работает только при N = 1, так как возникает ошибка деления на ноль. Но как говорится, чуть-чуть не считается).

Подробнее..

Recovery mode Задача о рюкзаке (Knapsack problem) простыми словами

05.06.2021 00:21:43 | Автор: admin

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

Хотел бы отметить книгу Grokking Algorithms автора Aditya Bhargava. У нее прям максимально простым языком расписаны основы алгоритмов. Так что если, вы как и я в универе думали что алгоритмы вам никогда не пригодятся, потому что FAANG это не для вас. То я вас и разочарую и обрадую, попасть туда может каждый, если конечно приложит достаточно усилий, ну а разочарую тем что вам конечно придется поднапрячься и осилить алгоритмы и чем раньше вы начнете это делать, тем лучше.

На хабре, уже есть одна статья на эту тему: Алгоритм решения задачи о рюкзаке ( версия 2, исправленная) / Хабр (habr.com) . Но, да простит меня автор, на мой взгляд она совершенно непонятная.

И так, перейду к делу. Сначала расскажу обо всем на пальцах, а потом мы рассмотрим решение на нашем любимом C#.

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

Вещи которые есть в магазинеВещи которые есть в магазине

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

Есть несколько способов решения. Один из них это перебор всех вариантов.

Для простоты, предмета будет только три, поскольку количество различных комбинаций в которых их можно унести растет очень быстро и даже для 3 предметов уже будет равно 8. Оно вычисляется по формуле 2n где n - количество предметов, то есть если предмета будет 4, то количество возможных комбинаций достигнет уже 16 и так далее. Такой вариант нас не устроит поскольку решая эту задачу онлайн на каком-нибудь Codility ваше решение зарубят с Timeout Exceeded. Нужно что-то получше.

Мы будем решать эту задачу методом динамического программирования

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

Заполним небольшую табличку:

1

2

3

4

Ожерелье / 4000 / 4 ед

Кольцо / 2500 / 1 ед

Подвеска / 2000 / 3 ед

С левой стороны у нас будут все вещи. При этом порядок в котором они расположены, в данной задаче нас не интересует. Количество колонок равно количеству потенциальных рюкзаков размером от 1, минимально возможного целого положительного числа, до размера нашего рюкзака, с шагом 1. Таким образом мы пытаемся решить ряд более мелких задач, для рюкзаков размером 1, 2, 3 и 4. Всегда ведь проще начинать с малого?)

1

2

3

4

Ожерелье / 4000 / 4 ед

0

0

0

4000

Сверху у нас заполненный первый ряд. Мы можем использовать в каждом ряду, только вещь из этого ряда или вещи из верхних рядов.

Рюкзак размером 1: Ожерелье не влезет в рюкзак, значит стоимость того что мы можем унести равна 0.

Рюкзак размером 2: Ожерелье не влезет в рюкзак, значит стоимость того что мы можем унести равна 0.

Рюкзак размером 3: Ожерелье не влезет в рюкзак, значит стоимость того что мы можем унести равна 0.

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

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

1

2

3

4

Ожерелье / 4000 / 4 ед

0

0

0

4000

Кольцо / 2500 / 1 ед

2500

2500

2500

4000

Добавилось кольцо. Делаем подсчеты для второго ряда:

Рюкзак размером 1: У нас добавилось кольцо и его размер как раз равен размеру даже самого маленького рюкзака. Но у нас ведь еще есть и ожерелье, мы уже проверили, оно не влезет. Кладем только кольцо.

Рюкзак размером 2: То же что и для размера 1. Кладем только кольцо.

Рюкзак размером 3: То же что и для размера 1. Кладем только кольцо.

Рюкзак размером 4: Здесь у нас есть выбор, либо положить только одно маленькое кольцо, либо взять ожерелье, которое тяжелее, но и стоит дороже. Забываем про кольцо, и возвращаемся за ожерельем!

Так, наконец добавим третий ряд

1

2

3

4

Ожерелье / 4000 / 4 ед

0

0

0

4000

Кольцо / 2500 / 1 ед

2500

2500

2500

4000

Подвеска / 2000 / 3 ед

2500

2500

2500

4500

Рюкзак размером 1: Кольцо дороже подвески, да и подвеска не влезет, она по всем параметрам проигрывает вещам выше, берем кольцо размером 1.

Рюкзак размером 2: Тоже самое что и для размера 1.

Рюкзак размером 3: Несмотря на то что мы можем здесь взять подвеску, кольцо выигрывает ее по параметрам, снова кладем ее.

Рюкзак размером 4: А вот тут у нас, весь рюкзак, и все возможные вещи. И мы видим что кольцо и подвеска вместе стоят на 500 долларов дороже ожерелья и при этом все так же влезут в рюкзак. Значит мы возьмем и кольцо и подвеску стоимостью 4500 и весом как раз 4 единицы.

В правом нижнем углу у нас верный результат.

Ну и где же здесь пере использование спросите вы? Да, мы им не пользовались, потому что таблица была сравнительно простая. Но, если присмотреться то можно заметить закономерность!

Представим что для количества вещей у нас счетчик i, а для рюкзаков j.

На примере последней ячейки рассмотрим формулу в действии

Зеленым выделена первая опция, красным вторая. Как видим стоимость в красном круге перевешивает стоимость в зеленомЗеленым выделена первая опция, красным вторая. Как видим стоимость в красном круге перевешивает стоимость в зеленом

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

И собственно алгоритм задачи на языке C#:

public static int[] weights = { 4, 1, 3 };public static int[] values = { 4000, 2500, 2000 };public static int CountMax(int[] weights, int[] values, int maxCapacity){    //строим массив и закладываем место на ячейки пустышки     //выходящие из левого верхнего угла    int[,] arr = new int[weights.Length + 1, maxCapacity + 1];    //проходим по всем вещам    for (int i = 0; i <= weights.Length; i++)    {        //проходим по всем рюкзакам        for (int j = 0; j <= maxCapacity; j++)        {            //попадаем в ячейку пустышку            if (i == 0 || j == 0)            {                arr[i, j] = 0;            }            else            {                   //если вес текущей вещи больше размера рюкзака                //казалось бы откуда значение возьмется для первой вещи                 //при таком условии. А оно возьмется из ряда пустышки                if (weights[i - 1] > j)                {                    arr[i, j] = arr[i - 1, j];                }                else                {                    //здесь по формуле. Значение над текущей ячейкой                    var prev = arr[i - 1, j];                    //Значение по вертикали: ряд вверх                    //и по горизонтали: вес рюкзака - вес текущей вещи                    var byFormula = values[i - 1] + arr[i - 1, j - weights[i - 1]];                    arr[i, j] = Math.Max(prev, byFormula);                }            }        }    }    // возвращаем правую нижнюю ячейку    return arr[weights.Length, maxCapacity];}

Всем побед и удачных собесов!

Подробнее..
Категории: Алгоритмы , C , Net , Algorithms , Knapsack problem

Распознаем номера автомобилей. Разработка multihead-модели в Catalyst

11.06.2021 08:06:47 | Автор: admin

Фиксация различных нарушений, контроль доступа, розыск и отслеживание автомобилей лишь часть задач, для которых требуется по фотографии определить номер автомобиля (государственный регистрационный знак или ГРЗ).

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

Сделать модель для распознавания можно с помощью разных подходов, например, путем поиска и определения отдельных символов, или в виде задачи image-to-text. Мы рассмотрим модель с несколькими выходами (multihead-модель). В качестве датасета возьмём датасет с российскими номерами от проекта Nomeroff Net. Примеры изображений из датасета представлены на рис. 1.

Рис. 1. Примеры изображений из датасета

Общий подход к решению задачи

Необходимо разработать модель, которая на входе будет принимать изображение ГРЗ, а на выходе отдавать строку распознанных символов. Модель будет состоять из экстрактора фичей и нескольких классификационных голов. В датасете представлены ГРЗ из 8 и 9 символов, поэтому голов будет девять. Каждая голова будет предсказывать один символ из алфавита 1234567890ABEKMHOPCTYX, плюс специальный символ - (дефис) для обозначения отсутствия девятого символа в восьмизначных ГРЗ. Архитектура схематично представлена на рис. 2.

Рис. 2. Архитектура модели

В качестве loss-функции возьмём стандартную кросс-энтропию. Будем применять её к каждой голове в отдельности, а затем просуммируем полученные значения для получения общего лосса модели. Оптимизатор Adam. Используем также OneCycleLRWithWarmup как планировщик leraning rate. Размер батча 128. Длительность обучения установим в 10 эпох.

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

Кодирование

Далее рассмотрим основные моменты кода. Класс датасета (листинг 1) в общем обычный для CV-задач на Pytorch. Обратить внимание стоит лишь на то, как мы возвращаем список кодов символов в качестве таргета. В параметре label_encoder передаётся служебный класс, который умеет преобразовывать символы алфавита в их коды и обратно.

class NpOcrDataset(Dataset):   def __init__(self, data_path, transform, label_encoder):       super().__init__()       self.data_path = data_path       self.image_fnames = glob.glob(os.path.join(data_path, "img", "*.png"))       self.transform = transform       self.label_encoder = label_encoder    def __len__(self):       return len(self.image_fnames)    def __getitem__(self, idx):       img_fname = self.image_fnames[idx]       img = cv2.imread(img_fname)       if self.transform:           transformed = self.transform(image=img)           img = transformed["image"]       img = img.transpose(2, 0, 1)             label_fname = os.path.join(self.data_path, "ann",                                  os.path.basename(img_fname).replace(".png", ".json"))       with open(label_fname, "rt") as label_file:           label_struct = json.load(label_file)           label = label_struct["description"]       label = self.label_encoder.encode(label)        return img, [c for c in label]

Листинг 1. Класс датасета

В классе модели (листинг 2) мы используем библиотеку PyTorch Image Models для создания экстрактора фичей. Каждую из классификационных голов модели мы добавляем в ModuleList, чтобы их параметры были доступны оптимизатору. Логиты с выхода каждой из голов возвращаются списком.

class MultiheadClassifier(nn.Module):   def __init__(self, backbone_name, backbone_pretrained, input_size, num_heads, num_classes):       super().__init__()        self.backbone = timm.create_model(backbone_name, backbone_pretrained, num_classes=0)       backbone_out_features_num = self.backbone(torch.randn(1, 3, input_size[1], input_size[0])).size(1)        self.heads = nn.ModuleList([           nn.Linear(backbone_out_features_num, num_classes) for _ in range(num_heads)       ])     def forward(self, x):       features = self.backbone(x)       logits = [head(features) for head in self.heads]       return logits

Листинг 2. Класс модели

Центральным звеном, связывающим все компоненты и обеспечивающим обучение модели, является Runner. Он представляет абстракцию над циклом обучения-валидации модели и отдельными его компонентами. В случае обучения multihead-модели нас будет интересовать реализация метода handle_batch и набор колбэков.

Метод handle_batch, как следует из названия, отвечает за обработку батча данных. Мы в нём будем только вызывать модель с данными батча, а обработку полученных результатов расчёт лосса, метрик и т.д. мы реализуем с помощью колбэков. Код метода представлен в листинге 3.

class MultiheadClassificationRunner(dl.Runner):   def __init__(self, num_heads, *args, **kwargs):       super().__init__(*args, **kwargs)       self.num_heads = num_heads    def handle_batch(self, batch):       x, targets = batch       logits = self.model(x)             batch_dict = { "features": x }       for i in range(self.num_heads):           batch_dict[f"targets{i}"] = targets[i]       for i in range(self.num_heads):           batch_dict[f"logits{i}"] = logits[i]             self.batch = batch_dict

Листинг 3. Реализация runnerа

Колбэки мы будем использовать следующие:

  • CriterionCallback для расчёта лосса. Нам потребуется по отдельному экземпляру для каждой из голов модели.

  • MetricAggregationCallback для агрегации лоссов отдельных голов в единый лосс модели.

  • OptimizerCallback чтобы запускать оптимизатор и обновлять веса модели.

  • SchedulerCallback для запуска LR Schedulerа.

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

  • CheckpointCallback чтобы сохранять лучшие веса модели.

Код, формирующий список колбэков, представлен в листинге 4.

def get_runner_callbacks(num_heads, num_classes_per_head, class_names, logdir):   cbs = [       *[           dl.CriterionCallback(               metric_key=f"loss{i}",               input_key=f"logits{i}",               target_key=f"targets{i}"           )           for i in range(num_heads)       ],       dl.MetricAggregationCallback(           metric_key="loss",           metrics=[f"loss{i}" for i in range(num_heads)],           mode="mean"       ),       dl.OptimizerCallback(metric_key="loss"),       dl.SchedulerCallback(),       *[           dl.AccuracyCallback(               input_key=f"logits{i}",               target_key=f"targets{i}",               num_classes=num_classes_per_head,               suffix=f"{i}"           )           for i in range(num_heads)       ],       dl.CheckpointCallback(           logdir=os.path.join(logdir, "checkpoints"),           loader_key="valid",           metric_key="loss",           minimize=True,           save_n_best=1       )   ]     return cbs

Листинг 4. Код получения колбэков

Остальные части кода являются тривиальными для Pytorch и Catalyst, поэтому мы не станем приводить их здесь. Полный код к статье доступен на GitHub.

Результаты эксперимента

Рис. 3. График лосс-функции модели в процессе обучения. Оранжевая линия train loss, синяя valid loss

В списке ниже перечислены некоторые ошибки, которые модель допустила на тест-сете:

  • Incorrect prediction: T970XT23- instead of T970XO123

  • Incorrect prediction: X399KT161 instead of X359KT163

  • Incorrect prediction: E166EP133 instead of E166EP123

  • Incorrect prediction: X225YY96- instead of X222BY96-

  • Incorrect prediction: X125KX11- instead of X125KX14-

  • Incorrect prediction: X365PC17- instead of X365PC178

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

Заключение

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

Спасибо за внимание! Надеемся, что наш опыт был вам полезен.

Больше наших статей по машинному обучению и обработке изображений:

Подробнее..

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

13.06.2021 16:20:24 | Автор: admin

Введение

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

Сегодня я расскажу о том, как можно спроецировать координаты с плоского изображения на карту. Эта короткая статья будет своеобразным продолжением первой статьи, в которой я рассказывал о базовых возможностях Mask R-CNN.

Статья была написана в сотрудничестве с @avdosev за что ему большое спасибо.

Проблема

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

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

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

Требования к камере:

Требование к изображению:

  • На изображении с камеры должна быть видна плоскость земли.

Способ 1.

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

Достоинства:

  • Алгоритм будет работать быстро, мы ограничены скоростью доступа к Map структуре данных, файлу или базе данных;

  • Сложных вычислений в рантайме нет.

Недостатки:

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

  • Требуется хранить информацию о каждом пикселе для каждой камеры на территории, что нецелесообразно;

  • Задавать для каждого пикселя координаты слишком долго и сложно.

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

Способ 2.

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

  1. Посмотреть на карту территории и изображения с камеры и постараться указать наиболее точные координаты углов;

  2. Зная параметры камеры (высота над землей, координаты камеры, угол наклона камеры и углы обзора), получить координаты углов видимости (трапеции), либо сразу вычислить координаты объекта.

Расчет положения объекта по координатам углов видимости

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

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

Область видимости камерыОбласть видимости камеры

Для простоты расчетов можно считать его трапецией.

Упрощенная до трапеции область видимости камерыУпрощенная до трапеции область видимости камеры

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

l_1 = \cfrac{C-B}{imageHeight - Y} + Bl_2 = \cfrac{D-A}{imageHeight - Y} + AM = \cfrac{l_2 - l_1}{imageWidth} * X + l_1

Где

  • l2 , l1 промежуточные переменные для вершины;

  • imageHeight, imageWidth высота и ширина изображения с камеры в пикселях соответственно;

  • A, B, C, D географические координаты вершин трапеции поля зрения камеры в формате {lat: float, lng: float};

  • X, Y координаты пикселей на изображении в декартовой системе координат, являются целыми числами;

  • M - результирующие координаты.

В случае Full HD картинки ширина и высота будут следующими: imageHeight=1080; imageWidth=1920.

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

  1. Брать центроид прямоугольника;

  2. Брать середину нижней стороны прямоугольника. Этот способ даст более точный результат, если объект перемещается по земле, а не летает;

Всё это можно объединить:

Взять 1/N высоты и центр по горизонтали, где N может изменяться в зависимости от различных факторов, например, типа объекта или способа перемещения.

Например, для N=8 мы получим такую результирующую точку на прямоугольнике объекта.

Все эти способы имеют существенную погрешность при малой высоте камеры или/и при большом наклоне камеры.

Расчет углов видимости камеры, используя ее характеристики

Для нахождения точек A, B, C, D автоматизированным образом, нам необходимо найти центр будущей трапеции C.

Зная высоту h и угол наклона камеры , мы можем найти противоположный катет len.

len = h * \tan(\alpha)

Зная координаты камеры (точка О) и её направление (в какую сторону она смотрит, угол ) можно найти центр её наблюдения (точка С). Найти ее можно по формуле:

С_x = O_x + cos(\beta) * lenC_y = O_y + sin(\beta) * len

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

\alpha = \arctan( \frac{\lvert С_x - О_x \rvert + \lvert C_y - O_y \rvert}{h})

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

Для основного угла +/- половина угла обзора по вертикали.

Для вторичного угла +/- половина угла обзора по горизонтали.

Примем горизонтальный угол обзора за viewAngleHorizontal, а вертикальный за viewAngleVertical.

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

Далее повторно рассмотрим точки трапеции. (Не стоит путать следующую точку C с центральной).

lenNear = h * tan(\alpha + viewAngleVertical / 2)A_x = O_x + cos(\beta - viewAngleHorizontal / 2 ) * lenNearA_y = O_y + sin(\beta - viewAngleHorizontal / 2 ) * lenNearB_x = O_x + cos(\beta - viewAngleHorizontal / 2 ) * lenNearB_y = O_y + sin(\beta - viewAngleHorizontal / 2 ) * lenNear lenFar = h * tan(\alpha - viewAngleVertical / 2)C_x = O_x + cos(\beta + viewAngleHorizontal / 2 ) * lenFarC_y = O_y + sin(\beta + viewAngleHorizontal / 2 ) * lenFarD_x = O_x + cos(\beta + viewAngleHorizontal / 2 ) * lenFarD_y = O_y + sin(\beta + viewAngleHorizontal / 2 ) * lenFar

Скомбинировав смещения по углам обзора, мы получаем координаты углов изображения - точки A, B, C, D.

Зная точки A, B, C, D можно получить географические координаты объекта. Но можно обойтись и без них. Следующий расчет потребует imageHeight, imageWidth, X, Y.

Если добавить вспомогательные оси, где координаты X, Y будут центром, то наш пиксель поделит изображение на 4 части. Определив отношения частей по горизонтали и по вертикали, мы можем определить углы, на которые должны делать смещение. Итоговая формула выглядит так:

len_M = h * tan(\alpha + viewAngleVertical * \frac{imageHeight - y}{imageHeight - 0.5})M_x = O_x + cos(\beta - viewAngleHorizontal * \frac{imageWidth - x}{imageWidth - 0.5} * len_MM_y = O_y + sin(\beta - viewAngleHorizontal * \frac{imageWidth - x}{imageWidth - 0.5} * len_M

Реализация на Python

imageWidth = 1920 # в данном примере зададим их константамиimageHeight = 1080import numpy as npdef geoToList(latlon):  return np.array((latlon['lat'], latlon['lng']))  def listToGeo(latlon):  return {'lat': latlon[0], 'lng': latlon[1] }  def getGeoCoordinates(A, B, C, D, X, Y):    A, B, C, D = list(map(geoToList, [A, B, C, D]))    vBC = (C - B) / imageHeight    vAD = (D - A) / imageHeight    latlonPixel1 = vBC * (imageHeight - Y) + B    latlonPixel2 = vAD * (imageHeight - Y) + A    vM = (latlonPixel2 - latlonPixel1) / imageWidth    M = vM * X + latlonPixel1    return listToGeo(M)

Результаты

Из этого изображения были получены координаты объекты левого верхнего и правого нижнего угла по X, Y соответственно - 613;233 1601;708.

Исходный код всегда доступен на Github.

Если найдете ошибки в алгоритме или в формулах, пожалуйста, сообщите об этом в комментариях.

Литература

Подробнее..

MEX (Minimum EXcluded) Алгоритм поиска минимального отсутствующего числа

13.06.2021 20:13:49 | Автор: admin
Добрый день. Сегодня хочется поговорить о том, как найти MEX (минимальное отсутствующие число во множестве).


Мы разберем три алгоритма и посмотрим на их производительность.

Добро пожаловать под cat

Предисловие


Перед тем как начать, хотелось бы рассказать почему я вообще за этот алгоритм взялся?
Всё началось с задачки на OZON.


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

И покопавшись в сети, оказалось, что нет общепринятого алгоритма нахождения MEX

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

static void Main(string[] args)        {            //MEX = 2            int[] values = new[] { 0, 12, 4, 7, 1 };                        //MEX = 5            //int[] values = new[] { 0, 1, 2, 3, 4 };                        //MEX = 24            //int[] values = new[] { 11, 10, 9, 8, 15, 14, 13, 12, 3, 2, 0, 7, 6, 5, 27, 26, 25, 4, 31, 30, 28, 19, 18, 17, 16, 23, 22, 21, 20, 43, 1, 40, 47, 46, 45, 44, 35, 33, 32, 39, 38, 37, 36, 58, 57, 56, 63, 62, 60, 51, 49, 48, 55, 53, 52, 75, 73, 72, 79, 77, 67, 66, 65, 71, 70, 68, 90, 89, 88, 95, 94, 93, 92, 83, 82, 81, 80, 87, 86, 84, 107, 106, 104 };                        //MEX = 1000            //int[] values = new int[1000];            //for (int i = 0; i < values.Length; i++) values[i] = i;                        //Импровизированный счетчик итераций            int total = 0;            int mex = GetMEX(values, ref total);            Console.WriteLine($"mex: {mex}, total: {total}");            Console.ReadKey();        }

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

1) Решение в лоб


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

  static int GetMEX(int[] values, ref int total)        {            for (int mex = 0; mex < values.Length; mex++)            {                bool notFound = true;                for (int i = 0; i < values.Length; i++)                {                    total++;                    if (values[i] == mex)                    {                        notFound = false;                        break;                    }                }                if (notFound)                {                    return mex;                }            }            return values.Length;        }    }


Максимально базовый случай. Сложность алгоритма составляет O(n*cell(n/2)) Т.к. для случая { 0, 1, 2, 3, 4 } нам нужно будет перебрать все числа т.к. совершить 15 операций. А для полностью заполного ряда из 100 числе 5050 операций Так себе быстродейственность.

2) Просеивание


Второй по сложности вариант в реализации укладывается в O(n) Ну или почти O(n), математики хитрят и не учитывают подготовку данных Ибо, как минимум, нам нужно знать максимальное число во множестве.
С точки зрения математики выглядит так.
Берется битовый массив S длинной m (где m максимально число в изначально массиве (V) + 1) заполненный 0. И в один проход исходному множеству (V) в массиве (S) ставятся 1. После этого в один проход находим первое пустое значение.
static int GetMEX(int[] values, ref int total)        {            //Не учитываем в сложности             var max = values.Max() + 1;            bool[] sieve = new bool[max];            for (int i = 0; i < values.Length; i++)            {                total++;                sieve[values[i]] = true;            }            for (int i = 0; i < sieve.Length; i++)            {                total++;                if (!sieve[i])                {                    return i;                }            }            return values.Length;        }

Т.к. математики хитры люди. То они говорят, что алгоритм O(n) ведь проход по массиву исходному массиву всего один
Т.к. математики хитры люди. И говорят, что алгоритм O(n) ведь проход по массиву исходному массиву всего один
Вот сидят и радуются, что такой крутой алгоритм придумали, но правда такова.
Первое нужно найти максимально число в исходном массиве O1(n)
Второе нужно пройтись по исходному массиву и отметить это значение в массиве S O2(n)
Третье нужно пройтись массиве S и найти там первую попавшеюся свободную ячейку O3(n)
Итого, т.к. все операции в целом не сложные можно упростить все расчеты до O(n*3)
Но это явно лучше решения в лоб Давайте проверим на наших тестовых данных:
1) Для случая { 0, 12, 4, 7, 1 }: В лоб: 11 итераций, просеивание: 13 итераций
2) Для случая { 0, 1, 2, 3, 4 }: В лоб: 15 итераций, просеивание: 15 итераций
3) Для случая { 11,}: В лоб: 441 итерация, просеивание: 191 итерация
4) Для случая { 0,,999}: В лоб: 500500 итераций, просеивание: 3000 итераций

Дело в том, что если отсутствующие значение является небольшим числом, то в таком случае решение в лоб оказывается быстрее, т.к. не требует тройного прохода по массиву. Но в целом, на больших размерностях явно проигрывает просеиванью, что собственно неудивительно.
С точки зрения математика алгоритм готов, и он великолепен, но вот с точки зрения программиста он ужасен из-за объема оперативной памяти, израсходованной впустую, да и финальный проход для поиска первого пустого значения явно хочется ускорить.
Давайте сделаем это, и оптимизируем код.
static int GetMEX(int[] values, ref int total)        {            total = values.Length;            var max = values.Max() + 1;            var size = sizeof(ulong) * 8;            ulong[] sieve = new ulong[(max / size) + 1];            ulong one = 1;            for (int i = 0; i < values.Length; i++)            {                total++;                sieve[values[i] / size] |= (one << (values[i] % size));            }            var maxInblock = ulong.MaxValue;            for (int i = 0; i < sieve.Length; i++)            {                total++;                if (sieve[i] != maxInblock)                {                    for (int j = 0; j < size; j++)                    {                        total++;                        if ((sieve[i] & (one << j)) == 0)                        {                            return i * size + j;                        }                    }                }            }            return values.Length;        }

Что мы тут сделали. Во-первых, в 64 раза уменьшили количество оперативной памяти, которая необходима.
var size = sizeof(ulong) * 8;ulong[] sieve = new ulong[(max / size) + 1];
Во-вторых, оптимизировали фальную проверку: мы проверяем сразу блок на вхождение первых 64 значений: if (sieve[i] != maxInblock) и как только убедились в том, что значение блока не равно бинарным 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111, только тогда ищем уже вхождение на уровне блока: ((sieve[i] & (one << j)) == 0
В итоге алгоритм просеивание нам дает следующие результат:
1) Для случая { 0, 12, 4, 7, 1 }: просеивание: 13 итераций, просеивание с оптимизацией: 13 итераций
2) Для случая { 0, 1, 2, 3, 4 }: В лоб: 15 итераций, просеивание с оптимизацией: 16 итераций
3) Для случая { 11,}: В лоб: 191 итерация, просеивание с оптимизацией: 191 итерации
4) Для случая { 0,,999}: В лоб: 3000 итераций, просеивание с оптимизацией: 2056 итераций

Так, что в итоге в теории по скорости?
O(n*3) мы превратили в O(n*2) + O(n / 64) в целом, чуть увеличили скорость, да еще объём оперативной памяти уменьшили аж в 64 раза. Что хорошо)

3) Сортировка


Как не сложно догадаться, самый простой способ найти отсутствующий элемент во множестве это иметь отсортированное множество.
Самый быстрый алгоритм сортировки это quicksort (быстрая сортировка), которая имеет сложность в O1(n log(n)). И итого мы получим теоретическую сложность для поиска MEX в O1(n log(n)) + O2(n)
static int GetMEX(int[] values, ref int total)        {            total = values.Length * (int)Math.Log(values.Length);            values = values.OrderBy(x => x).ToArray();            for (int i = 0; i < values.Length - 1; i++)            {                total++;                if (values[i] + 1 != values[i + 1])                {                    return values[i] + 1;                }            }            return values.Length;        }

Шикарно. Ничего лишнего)
Проверим количество итераций
1) Для случая { 0, 12, 4, 7, 1 }: просеивание с оптимизацией: 13, сортировка: ~7 итераций
2) Для случая { 0, 1, 2, 3, 4 }: просеивание с оптимизацией: 16 итераций, сортировка: ~9 итераций
3) Для случая { 11,}: просеивание с оптимизацией: 191 итерации, сортировка: ~356 итераций
4) Для случая { 0,,999}: просеивание с оптимизацией: 2056 итераций, сортировка: ~6999 итераций

Здесь указаны средние значения, и они не совсем справедливы. Но в целом: сортировка не требует дополнительной памяти и явно позволяет упростить последний шаг в переборе.
Примечание: values.OrderBy(x => x).ToArray() да я знаю, что тут выделилась память, но если делать по уму, то можно изменить массив, а не копировать его


Вот у меня и возникла идея оптимизировать quicksort для поиска MEX. Данный вариант алгоритма я не находил в интернете, ни с точки зрения математики, и уж тем более с точки зрения программирования. То код будем писать с 0 по дороге придумывая как он будет выглядеть :D

Но, для начала, давайте вспомним как вообще работает quicksort. Я бы ссылку дал, но нормально пояснения quicksort на пальцах фактически нету, создается ощущение, что авторы пособий сами разбираются в алгоритме пока его рассказывают про него
Так вот, что такое quicksort:
У нас есть неупорядоченный массив { 0, 12, 4, 7, 1 }
Нам потребуется случайное число, но лучше взять любое из массива, это называется опорное число (T).
И два указателя: L1 смотрит на первый элемент массива, L2 смотрит на последний элемент массива.
0, 12, 4, 7, 1
L1 = 0, L2 = 1, T = 1 (T взял тупа последние)

Первый этап итерации:
Пока работам только с указателем L1
Сдвигаем его по массиву вправо пока не найдем число больше чем наше опорное.
В нашем случае L1 равен 8

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

Третей этап итерации:
Меняем числа в указателях L1 и L2 местами, указатели не двигаем.
И переходим к первому этапу итерации.
Эти этапы мы повторяем до тех пор, пока указатели L1 и L2 не будет равны, не значения по ним, а именно указатели. Т.е. они должны указывать на один элемент.

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

Напишем Quicksort
static void Quicksort(int[] values, int l1, int l2, int t, ref int total)        {            var index = QuicksortSub(values, l1, l2, t, ref total);            if (l1 < index)            {                Quicksort(values, l1, index - 1, values[index - 1], ref total);            }            if (index < l2)            {                Quicksort(values, index, l2, values[l2], ref total);            }        }        static int QuicksortSub(int[] values, int l1, int l2, int t, ref int total)        {            for (; l1 < l2; l1++)            {                total++;                if (t < values[l1])                {                    total--;                    for (; l1 <= l2; l2--)                    {                        total++;                        if (l1 == l2)                        {                            return l2;                        }                        if (values[l2] <= t)                        {                            values[l1] = values[l1] ^ values[l2];                            values[l2] = values[l1] ^ values[l2];                            values[l1] = values[l1] ^ values[l2];                            break;                        }                    }                }            }            return l2;        }


Проверим реальное количество итераций:
1) Для случая { 0, 12, 4, 7, 1 }: просеивание с оптимизацией: 13, сортировка: 11 итераций
2) Для случая { 0, 1, 2, 3, 4 }: просеивание с оптимизацией: 16 итераций, сортировка: 14 итераций
3) Для случая { 11,}: просеивание с оптимизацией: 191 итерации, сортировка: 1520 итераций
4) Для случая { 0,,999}: просеивание с оптимизацией: 2056 итераций, сортировка: 500499 итераций

Попробуем поразмышлять вот над чем. В массиве { 0, 4, 1, 2, 3 } нет недостающих элементов, а его длина равно 5. Т.е. получается, массив в котором нет отсутствующих элементов равен длине массива 1. Т.е. m = { 0, 4, 1, 2, 3 }, Length(m) == Max(m) + 1. И самое главное в этом моменте, что это условие справедливо, если значения в массиве переставлены местами. И важно то, что это условие можно распространить на части массива. А именно вот так:
{ 0, 4, 1, 2, 3, 12, 10, 11, 14 } зная, что в левой части массива все числа меньше некого опорного числа, например 5, а в правой всё что больше, то нет смысла искать минимальное число слева.
Т.е. если мы точно знаем, что в одной из частей нет элементов больше определённого значения, то само это отсутствующие число нужно искать во второй части массива. В целом так работает алгоритм бинарного поиска.
В итоге у меня родилась мысль упростить quicksort для поиска MEX объединив его с бинарным поиском. Сразу скажу нам не нужно будет полностью отсортировывать весь массив только те части, в которых мы будем осуществлять поиск.
В итоге получаем код
static int GetMEX(int[] values, ref int total)        {            return QuicksortMEX(values, 0, values.Length - 1, values[values.Length - 1], ref total);        }        static int QuicksortMEX(int[] values, int l1, int l2, int t, ref int total)        {            if (l1 == l2)            {                return l1;            }            int max = -1;            var index = QuicksortMEXSub(values, l1, l2, t, ref max, ref total);            if (index < max + 1)            {                return QuicksortMEX(values, l1, index - 1, values[index - 1], ref total);            }            if (index == values.Length - 1)            {                return index + 1;            }            return QuicksortMEX(values, index, l2, values[l2], ref total);        }        static int QuicksortMEXSub(int[] values, int l1, int l2, int t, ref int max, ref int total)        {            for (; l1 < l2; l1++)            {                total++;                if (values[l1] < t && max < values[l1])                {                    max = values[l1];                }                if (t < values[l1])                {                    total--;                    for (; l1 <= l2; l2--)                    {                        total++;                        if (values[l2] == t && max < values[l2])                        {                            max = values[l2];                        }                        if (l1 == l2)                        {                            return l2;                        }                        if (values[l2] <= t)                        {                            values[l1] = values[l1] ^ values[l2];                            values[l2] = values[l1] ^ values[l2];                            values[l1] = values[l1] ^ values[l2];                            break;                        }                    }                }            }            return l2;        }

Проверим количество итераций
1) Для случая { 0, 12, 4, 7, 1 }: просеивание с оптимизацией: 13, сортировка MEX: 8 итераций
2) Для случая { 0, 1, 2, 3, 4 }: просеивание с оптимизацией: 16 итераций, сортировка MEX: 4 итераций
3) Для случая { 11,}: просеивание с оптимизацией: 191 итерации, сортировка MEX: 1353 итераций
4) Для случая { 0,,999}: просеивание с оптимизацией: 2056 итераций, сортировка MEX: 999 итераций

Итого


Мы получили разны варианты поиска MEX. Какой из них лучше решать вам.
В целом. Мне больше всех нравится просеивание, и вот по каким причинам:
У него очень предсказуемое время выполнения. Более того, этот алгоритм можно легко использовать в многопоточном режиме. Т.е. разделить массив на части и каждую часть пробегать в отдельном потоке:
for (int i = minIndexThread; i < maxIndexThread; i++)sieve[values[i] / size] |= (one << (values[i] % size));

Единственное, нужен lock при записи sieve[values[i] / size]. И еще алгоритм идеален при выгрузки данных из базы данных. Можно грузить пачками по 1000 штук например, в каждом потоке и всё равно он будет работать.
Но если у нас строгая нехватка памяти, то сортировка MEX явно выглядит лучше.

П.с.
Я начал рассказ с конкурса на OZON в котором я пробовал участвовал, сделав предварительный вариант алгоритма просеиванья, приз за него я так и не получил, OZON счел его неудовлетворительным По каким именно причин он так и не сознался До и кода победителя я не видел. Может у кого-то есть идеи как можно решить задачу поиска MEX лучше?
Подробнее..

Корни разные нужны, корни разные важны

14.06.2021 16:14:45 | Автор: admin

Вместо вступления

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

Интересен алгоритм sqrxi32 от @Sdima1357 Пример 1, далее для краткости именуемый как _i32. Алгоритм _i32 безусловно выполняет главное условие задачи округление до ближайшего целого на всём множестве значений аргумента [ 0 .. 0xFFFFFFFF ], при этом показывает высокую производительность.

Пример 1: Вычисление квадратного корня из целого с округлением до ближайшего целого.

uint16_t sqrxi32( uint32_t y ){if ( y == 1 )return 1;uint32_t xh = y > 0x10000ul ? 0x10000ul : y;uint32_t xl = 0;uint32_t xc;for ( int k = 0; k < 16; k++ ){xc = ( xh + xl ) >> 1ul;if ( xc * xc - xc >= y ){xh = xc;}else{xl = xc;}}return ( xh + xl ) >> 1ul;}

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

О чём этот текст

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

Исходный код содержит содержит решение одной задачи разными алгоритмами.

Анализ результатов наблюдений за рамками настоящей публикации.

Условия и допуски

Для сокращение текста принимаем:

  • аппаратных платформ для тестов 3 платформы;

  • вариантов оптимизации сборки 3 значения

Для сборки двоичного кода применяем:

  • Одну единицу компиляции теста (файл main.c)

  • Компиляцию в моно-поточный исполняемый файл

  • Единую сборочную среду: CubeIDE (она же Eclipce CDT)

  • Стандартные настройки профиля сборки RELEASE в среде CubeIDE

  • Единый диалект компилятора: ISO C11 + gnu extensions (-std=gnu11)

  • Применительно к микроконтроллерам:

    • CubeMX default settings, +48MHz, +USART1, +HAL;

    • Runtime lib: Reduced C ( --spec=nano.specs );

    • Use float with printf from new lib nano ( -u _printf_float );

Таблица 1: Варианты сборки исполняемого кода

Таблица 2: Общие характеристики аппаратных платформ

Таблица 3: Технические характеристики аппаратных платформ

Тестовый набор содержит некоторые алгоритмы из предыдущей статьи и комментариев к ней.

Для оценки FPU платформы M4 в тестовый набор добавлена функция sqrt_fps, решающая вычислительную задачу с применением коротких действительных (float), именуемая далее _fps (Float Point Short) Пример 2.

Пример 2: Квадратный корень из целого с точностью float

uint16_t sqrt_fps( uint32_t number ){if ( number < 2 )return (uint16_t) number;float f_rslt = sqrtf( number );uint32_t rslt = (uint32_t) f_rslt;if ( !( f_rslt - (float) rslt < .5 ) )rslt++;return (uint16_t) rslt;}

Функция _fps работает без ошибок с аргументом менее 22-х бит, что соответствует десятичному порядку 1+E5 Иллюстрация 1.

Иллюстрация 1: Ошибки функции "_fps" на порядках 1+E6+

Для всех наблюдаемых алгоритмов ограничиваем диапазон аргумента множеством значений
[0 .. 1+E5].

Таблица 4: Список наблюдаемых алгоритмов

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

Относительная производительность платформ

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

Иллюстрация 2: Относительная производительность аппаратных платформ

На левой части графика по оси Y отображается среднее время последовательного выполнения всех тестов (Таблица 4), измеренное в секундах. На оси X аппаратные платформы.

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

График каждой платформы, в свою очередь, представлен тремя столбцами, демонстрирующими зависимость производительности от варианта оптимизации сборки: -O0, -Os, -O3.

Правая часть графика (Иллюстрация 2) показывает относительный прирост производительности у каждой аппаратной платформы в зависимости от варианта оптимизации сборки: -O0, -Os, -O3.

Производительность 100% демонстрирует двоичный код, собранный без оптимизации ( -O0 ). Это базовая производительность платформы.

Чем выше высота столбика относительно базовой производительности (O0) в правой части графика, тем лучше оптимизационные возможности программно-аппаратной платформы.

Наблюдаем наибольший прирост производительности от оптимизации на этапе сборки наплатформе M4.

Платформа x86

На графике (Иллюстрация 3) по оси Y отображается число цикличных вызовов наблюдаемых функций за одну миллисекунду. На оси X наблюдаемые функции (Таблица 4).

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

Цветом на оси X обозначен способ оптимизации на этапе сборки. Соответствие цвета и характера оптимизации отражает легенда.

Иллюстрация 3: Производительность алгоритмов на платформе x86

Платформа x86 максимально раскрывает преимущества алгоритмов с плавающей точкой перед целочисленными.

Заслуживает отдельного внимания часть графика в оранжевом контуре.

Производительность кода без оптимизации (O0) лучше на 39% для алгоритма _fpu (Os) и на 16% для алгоритма _fps (O3). Другими словами, любая оптимизация на этапе сборки снижает производительность платформы x86 на действительных числах.

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

Платформа M4

Платформа M4 демонстрирует предсказуемый результат (Иллюстрация 4).

Иллюстрация 4: Производительность алгоритмов на платформе M4

Модуль с плавающей точкой M4 даёт ожидаемый прирост производительности для алгоритма _fps, основанного на коротком действительном float.

Последнее подтверждается результатом сравнения производительности алгоритмов при отключенном модуле FPU на платформе M4 Иллюстрация 5.

Наблюдая графики помним, что точность вычислений алгоритма _fps гарантируется в диапазоном 1+E5 (см. Иллюстрация 1) без относительно того, включен ли модуль FPU на M4 или нет.

Иллюстрация 5: Производительность алгоритмов на платформе M4 без FPU

Платформа M0

Результаты платформы M0 похожи на результаты платформы M4безFPU (Иллюстрация 5), только медленнее Иллюстрация 6.

Заметим, тактовая частота при тестировании устанавливалась одинаковой и для M4, и для M0 48 MHz. Однако, производительность M0 хуже в два с лишним раза, чем M4, в условиях равенства прочих характеристик.

Иллюстрация 6: Производительность алгоритмов на платформе M0

Алгоритм _fps на платформе M0 ожидаемо опережает в два раза алгоритм _fpu.

Целочисленные алгоритмы опережают алгоритмы с плавающей точкой.

По странному стечению обстоятельств в заключительном графике (Иллюстрация 6) снова есть место для оранжевого контура.

При сборке без оптимизации (O0) алгоритм _evn быстрее алгоритма _i32. И алгоритм _evn медленнее, чем _i32, если сборка проводится с оптимизацией.

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

Вместо заключения

Производительность программы зависит от многих причин.

Оптимизация на этапе сборки способна ухудшать производительность, замедлять программу, что демонстрируют экспериментальные данные в оранжевых контурах выше (Иллюстрация 3 и Иллюстрация 6).

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

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

Приложение 1. Порядок тестирования платформы x86

  1. Создать в среде CubeIDE (Eclipse CDT) проект C штатным способом

  2. Написать текст программы Пример 3

  3. Добавить в проект файл sqrt_cmp.h Пример 6

  4. Осуществить сборку и запуск программы:

    1. штатными средствами IDE;

    2. или из командной строки Пример 4

  5. Меняя вид оптимизации ( -O0, -O3, -Os ) наблюдать результат.

Пример 3: Исходный текст программы x86 main.c

#include sqrt_cmp.hint main( void ){main_Of_SqrtComp();return 0;}

Пример 4 Запуск теста на платформе x86 из терминала

gcc main.c -o main -I. -Wall -lm -std=gnu11 -O3 && ./main

Запуск теста из терминала платформы x86 предполагает, что файлы main.c и sqrt_cmp.h располагаются в одном каталоге, и этот каталог выбран рабочим (pwd).

Иллюстрация 7: Запуск теста из терминала x86

Приложение 2. Порядок тестирования платформы STM32

  1. Создать в среде CubeIDE проект STM32 штатным способом (CubeMX)

  2. Добавить файл sqrt_cmp.h в проект STM32 Пример 6

  3. Включить sqrt_cmp.h в файл main.c Пример 5

  4. Осуществить сборку и запуск программы штатными средствами IDE

  5. Меняя вид оптимизации ( -O0, -O3, -Os ) наблюдать результат

Пример 5: Исходный текст для STM32 (с пропусками < ... >) main.c

<  >/* Private includes ----------------------------------------------------------*//* USER CODE BEGIN Includes */#include "sqrt_cmp.h"/* USER CODE END Includes */<  >/**  * @brief  The application entry point.  * @retval int  */int main(void){<  >  /* Infinite loop */  /* USER CODE BEGIN WHILE */  main_Of_SqrtComp();  while (1)  {    /* USER CODE END WHILE */    /* USER CODE BEGIN 3 */  }  /* USER CODE END 3 */

Приложение 3. Порядок тестирования других алгоритмов и платформ

Сборка теста для других платформ проводится по аналогии.

Для отличных от упомянутых выше аппаратных платформ (Таблица 3), вероятно, потребуется косметическая модификация файла sqrt_cmp.h.

Пример 6: Содержание файла sqrt_cmp.h

/****************************************************************************** * File: sqrt_cmp.h Created on 5 авг. 2020 г. * CC0 1.0 Universal (CC0 1.0) * Creative Commons Public Domain Dedication * No Copyright * * TAB Size .EQ 4 ********************************************************************************/#ifndef __SQRT_CMP_H#define __SQRT_CMP_H#include<math.h>#include<stdio.h>#include<stdint.h>#ifdef __cplusplusextern "C" {#endif/****************************************************************************** * Interface of the entry point for all sqrt tests ******************************************************************************/void main_Of_SqrtComp();/****************************************************************************** * test case selection: TEST_SET * select one of the test suite via a comment. ******************************************************************************/#define TEST_SETTEST_ALL//#define TEST_SETTEST_ROUNDING//#define TEST_SETTEST_PERFORMANCE/****************************************************************************** * Interfaces of test functions. * See implementation of them at the end of this file. ******************************************************************************/typedef uint16_t (*sqrt_func)( uint32_t number );uint16_t sqrt_fpu( uint32_t number );// floating point function from articleuint16_t sqrt_evn( uint32_t number );// integer function from articleuint16_t sqrxi32( uint32_t y );// integer function from comment byuint16_t sqrt_fps( uint32_t number );// optimized floating point function for Cortex M4// <-- insert interface of your function here/****************************************************************************** * Set to variable named as 'round_test_func' below * to the alias of one of the functions above. * The NULL will select last function in comp_list[] ******************************************************************************/sqrt_func round_test_func = sqrt_fps;// specific instance for the rounding test//sqrt_func round_test_func = sqrxi32;// specific instance for the rounding test//sqrt_func round_test_func = sqrt_evn;// specific instance for the rounding test//sqrt_func round_test_func = NULL;// last function in comp_list[]/****************************************************************************** * The array of test functions for competing routines is called comp_list[]. * Adding a new function to the test: - copy the implementation of the new function to the end of this file; - declare the function interface at the beginning of this file; - add the alias and declaration of the new function to end of array named comp_list[]. ******************************************************************************/// @formatter:offtypedef struct{sqrt_funcfsqrt;char *alias;} SCompFunc;SCompFunc comp_list[] =// competition list{{ sqrt_fpu, "_fpu" },{ sqrt_fps, "_fps" },{ sqrt_evn, "_evn" },{ sqrxi32,  "_i32" }// <-- insert your function name & alias here};/* @formatter:on *//****************************************************************************** * Platform-independent definitions ******************************************************************************/#define PUT_FORMAT_MSG(f_, ...) { \sprintf( (char *)s_buf, (char *)f_, ##__VA_ARGS__ ); \PUT_MSG( (char *)s_buf ); }#define MS_PER_SEC1000#define US_PER_SEC( MS_PER_SEC * MS_PER_SEC )#define ARRAY_SIZE(a) (sizeof a / sizeof *a)// size of static array at runtime#define SIRV(f) if ( f ) ;// suppress Ignore Return Value warning/****************************************************************************** * Platform-specific defines ******************************************************************************/#if defined( USE_HAL_DRIVER )// STM32 ARM Cortex platform#include<string.h>#include "main.h"//*****************************************************************************// Platform-specific defines for the helper functions#define SCALE_RATE1// must be .GE than 1#define X_CLOCKHAL_GetTick()#define X_DELAY( ms )HAL_Delay( ms )//*****************************************************************************// Platform-specific defines for the terminal output#define USART_HANDLEhuart1// set valid USART handler alias here defined by the config of MCU#define USART_TIMEOUT150// max timeout for HAL_UART_Transmitextern UART_HandleTypeDef USART_HANDLE;extern HAL_StatusTypeDef HAL_UART_Transmit ( UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout );#define PUT_MSG( msg ) \HAL_UART_Transmit( &USART_HANDLE, (uint8_t *)msg, strlen( (char *)msg ), USART_TIMEOUT )#define CPU_CLOCK_MHz( SystemCoreClock / US_PER_SEC )// CPU CLK in MHz#if defined( STM32F0 )#defineCPU_ID ( "STM32 ARM Cortex M0" )#elif defined ( STM32F3 )#defineCPU_ID ( "STM32 ARM Cortex M4" )#else#defineCPU_ID ( "Maybe STM32 ARM Cortex" )#endif#define PUT_SYS_INFOPUT_FORMAT_MSG( " %s @ "fdU()" MHz\n", CPU_ID, CPU_CLOCK_MHz )#else// #if defined( USE_HAL_DRIVER)#include <time.h>#include <stdlib.h>//*****************************************************************************// Platform-specific defines for the helper functions#define SCALE_RATE100// must be .GE than 1#define X_CLOCK(uint32_t) x_clock()#define X_DELAY( ms )x_delay( ms )uint32_t x_clock(){uint64_t result = (uint64_t) clock();result *= MS_PER_SEC;result /= CLOCKS_PER_SEC;return (uint32_t) result;}void x_delay( uint32_t ms ){uint64_t tm = x_clock();while ( ( x_clock() - tm ) < ms );}//*****************************************************************************// Platform-specific defines for the terminal output#define PUT_MSG( msg ) \printf( "%s", (char *)msg ), fflush ( stdout );#if defined( __unix__ )// anybody other platform for gcc#define PUT_SYS_INFOSIRV( system( "cat /proc/cpuinfo | grep 'model name' | head -1 | sed s/'model name\t:'/''/" ) )#else#define PUT_SYS_INFOPUT_MSG( "Undefined System & CPU" )#endif// #if defined( __unix__ )  // anybody other platform for gcc#endif// #if defined( USE_HAL_DRIVER)#if  ( __WORDSIZE == 64 )#define fdI(s)"%" #s "d"#define fdU(s)"%" #s "u"#define fdX(s)"%" #s "x"#else// let's say __WORDSIZE == 32#define fdI(s)"%" #s "ld"#define fdU(s)"%" #s "lu"#define fdX(s)"%" #s "lx"#endif// #if ( __WORDSIZE == 64 )#if defined ( DEBUG ) || defined ( _DEBUG ) // chk build mode of CubeIDE#defineBUILD_MODE"DEBUG"#else // Maybe Release#defineBUILD_MODE"RELEASE"#endif// #if defined ( DEBUG ) || defined ( _DEBUG )/****************************************************************************** * the helper data with testing ranges ******************************************************************************/// @formatter:offtypedef struct{uint32_tstart;uint32_tstop;uint32_trepeat;} STestRange;STestRangetest_rngs[] ={{ 0, 1000, 100 * SCALE_RATE },{ 0, 10000, 10 * SCALE_RATE },{ 0, 100000, 1 * SCALE_RATE }};uint32_t test_results[ARRAY_SIZE( test_rngs )][ARRAY_SIZE( comp_list ) + 1];#define MSG_BUFF_SIZE512uint8_t s_buf[MSG_BUFF_SIZE];// buffer for a terminal output/* @formatter:on *//****************************************************************************** * Test sets definitions. Do not change it. ******************************************************************************/#define TEST_ROUNDING1#define TEST_PERFORMANCE2#define TEST_ALL( TEST_ROUNDING | TEST_PERFORMANCE )#ifndef TEST_SET#defineTEST_SETTEST_ALL#endif#define HI_ROUND_TEST_RANGE_END0x007FFFFFUL#define HI_ROUND_TEST_RANGE_START( HI_ROUND_TEST_RANGE_END >> 4 )/****************************************************************************** * Interface of helper functions ******************************************************************************/void main_Header();void testRounding();void testPerformance();/****************************************************************************** * Implementation of the entry point for all sqrt tests ******************************************************************************/void main_Of_SqrtComp(){X_DELAY( MS_PER_SEC / 2 );// suppress the output of a previous instance// while the new instance is loading into the MCUuint32_t start_time = X_CLOCK;main_Header();// checking normal and extended ranges for roundingif ( TEST_SET & TEST_ROUNDING )testRounding();// checking normal ranges on execution timeif ( TEST_SET & TEST_PERFORMANCE )testPerformance();uint32_t test_time = X_CLOCK - start_time;uint32_t test_m = ( test_time / MS_PER_SEC ) / 60;uint32_t test_s = ( test_time / MS_PER_SEC ) % 60;uint32_t test_ms = test_time % MS_PER_SEC;PUT_FORMAT_MSG( "\ndone, spent time: "fdU()" m, "fdU()"."fdU()" s\n", test_m, test_s, test_ms );}/****************************************************************************** * Implementation of the helper functions ******************************************************************************/void main_Header(){PUT_MSG( "\n\n**********************************************************\n" );PUT_SYS_INFO;PUT_FORMAT_MSG( "*********** %s, built at %s\n", BUILD_MODE, __TIME__ );}void testPerformance(){uint32_t i_func, i_rpt, i_rng;uint32_t number, first, second, diff;uint64_t temp;PUT_MSG( "----------+ Performance test" );for ( i_rng = 0; i_rng < ARRAY_SIZE( test_rngs ); i_rng++ ){PUT_MSG( "\n" );PUT_FORMAT_MSG( "test range:["fdU()".."fdU()"], repeat="fdU()"\n", test_rngs[i_rng].start, test_rngs[i_rng].stop,test_rngs[i_rng].repeat );test_results[i_rng][0] = test_rngs[i_rng].stop;for ( i_func = 0; i_func < ARRAY_SIZE( comp_list ); i_func++ ){PUT_FORMAT_MSG( "%s ... ", comp_list[i_func].alias );first = X_CLOCK;for ( i_rpt = 0; i_rpt < test_rngs[i_rng].repeat; i_rpt++ )for ( number = test_rngs[i_rng].start; number < test_rngs[i_rng].stop; number++ )comp_list[i_func].fsqrt( number );second = X_CLOCK;diff = second - first;temp = ( test_rngs[i_rng].stop - test_rngs[i_rng].start ) * test_rngs[i_rng].repeat;test_results[i_rng][i_func + 1] = (uint32_t) ( temp / diff );if ( i_func < ARRAY_SIZE( comp_list ) - 1 )PUT_MSG( ", " );}}// small reportPUT_FORMAT_MSG( "\n----------+ Report: sqrt`s calls per ms\n%10s", "range" );for ( i_func = 0; i_func < ARRAY_SIZE( comp_list ); i_func++ )PUT_FORMAT_MSG( "%10s", comp_list[i_func].alias );for ( i_rng = 0; i_rng < ARRAY_SIZE( test_rngs ); i_rng++ ){PUT_MSG( "\n" );for ( i_func = 0; i_func < ARRAY_SIZE( comp_list ) + 1; i_func++ )PUT_FORMAT_MSG( fdU( 10 ), test_results[i_rng][i_func] );}PUT_FORMAT_MSG( "\n----------+\n%10s", "average" );for ( i_func = 0; i_func < ARRAY_SIZE( comp_list ); i_func++ ){temp = 0;for ( i_rng = 0; i_rng < ARRAY_SIZE( test_rngs ); i_rng++ )temp += test_results[i_rng][i_func + 1];temp /= ARRAY_SIZE( test_rngs );PUT_FORMAT_MSG( fdU( 10 ), (uint32_t)temp );}}void testRoundingFunction( uint32_t start, uint32_t finish, sqrt_func psqrt, char *fname );void testRounding(){uint16_t i_rng;uint16_t f_rng;PUT_MSG( "----------+ Rounding test\n" );// checking the existence for the test functionfor ( f_rng = 0; f_rng < ARRAY_SIZE( comp_list ); f_rng++ )if ( comp_list[f_rng].fsqrt == round_test_func )break;if ( !( f_rng < ARRAY_SIZE( comp_list ) ) ){f_rng = ARRAY_SIZE( comp_list ) - 1;PUT_FORMAT_MSG( "Value of 'round_test_func' not found.\n" );}PUT_FORMAT_MSG( "Function '%s' is tested for rounding.\n", comp_list[f_rng].alias );// checking standard rangesfor ( i_rng = 0; i_rng < ARRAY_SIZE( test_rngs ); i_rng++ )testRoundingFunction( test_rngs[i_rng].start, test_rngs[i_rng].stop, comp_list[f_rng].fsqrt, comp_list[f_rng].alias );// checking extended rangetestRoundingFunction( HI_ROUND_TEST_RANGE_START, HI_ROUND_TEST_RANGE_END, comp_list[f_rng].fsqrt, comp_list[f_rng].alias );}void turn_the_fan( uint32_t ms );void testRoundingFunction( uint32_t start, uint32_t finish, sqrt_func psqrt, char *fname ){uint32_t rf, ri;uint32_t n, c = 0;PUT_FORMAT_MSG( "test range:["fdU( 10 )".."fdU( 10 )"] ... ", start, finish );for ( n = start; n < finish; n++ ){rf = sqrt_fpu( n );ri = ( *psqrt )( n );if ( rf != ri ){if ( c++ > 3 ){PUT_FORMAT_MSG( "\b\n(!)too many mistakes in '%s', ", fname );break;}else{double d = sqrt( (double) n );PUT_FORMAT_MSG( "\b\n%s("fdU( 10 )")="fdU()" != "fdU(), fname, n, ri, rf );PUT_FORMAT_MSG( " (real value is %.6lf)", d );}}turn_the_fan( MS_PER_SEC );}if ( !c ){PUT_FORMAT_MSG( "\b done.\n" );}else{PUT_FORMAT_MSG( "test failed.\n" );}}void turn_the_fan( uint32_t ms ){static char ca[] = "|/-\\";static uint32_t cs = ARRAY_SIZE(ca) - 1;static uint32_t cn = 0;static uint32_t at = 0;uint32_t ct = X_CLOCK;if ( ct - at > ms ){at = ct;PUT_FORMAT_MSG( "\b%c", ca[cn++ % cs] );}}/****************************************************************************** * Implementation of the sqrt functions ******************************************************************************/// floating point arg & result with doubleuint16_t sqrt_fpu( uint32_t number ){if ( number < 2 )return (uint16_t) number;double f_rslt = sqrt( number );uint32_t rslt = (uint32_t) f_rslt;if ( !( f_rslt - (double) rslt < .5 ) )rslt++;return (uint16_t) rslt;}// floating point arg & result with floatuint16_t sqrt_fps( uint32_t number ){if ( number < 2 )return (uint16_t) number;float f_rslt = sqrtf( number );uint32_t rslt = (uint32_t) f_rslt;if ( !( f_rslt - (float) rslt < .5 ) )rslt++;return (uint16_t) rslt;}// unsigned integer arg & result// @formatter:offuint16_t sqrt_evn ( uint32_t number ){if ( number < 2 )return ( uint16_t ) number;uint32_t temp;uint32_t div;uint32_t rslt;if ( number & 0xFFFF0000L )if ( number & 0xFF000000L )if ( number & 0xF0000000L )if ( number & 0xE0000000L )div = 43771;elsediv = 22250;elseif ( number & 0x0C000000L )div = 11310;elsediv = 5749;elseif ( number & 0x00F00000L )if ( number & 0x00C00000L )div = 2923;elsediv = 1486;elseif ( number & 0x000C0000L )div = 755;elsediv = 384;elseif ( number & 0xFF00L )if ( number & 0xF000L )if ( number & 0xC000L )div = 195;elsediv = 99;elseif ( number & 0x0C00L )div = 50;elsediv = 25;elseif ( number & 0xF0L )if ( number & 0x80L )div = 13;elsediv = 7;elsediv = 3;rslt = number;while ( 1 ){temp = number / div;temp += div;div = temp >> 1;div += temp & 1;if ( rslt > div )rslt = div;else{if ( number / rslt == rslt - 1 && number % rslt == 0 )rslt--;return ( uint16_t ) rslt;}}}/* @formatter:on */// unsigned integer arg & resultuint16_t sqrxi32( uint32_t y ){if ( y == 1 )return 1;uint32_t xh = y > 0x10000ul ? 0x10000ul : y;uint32_t xl = 0;uint32_t xc;for ( int k = 0; k < 16; k++ ){xc = ( xh + xl ) >> 1ul;if ( xc * xc - xc >= y ){xh = xc;}else{xl = xc;}}return ( xh + xl ) >> 1ul;}// <-- insert implementation of your function sqrt here#ifdef __cplusplus}#endif#endif // __SQRT_CMP_H
Подробнее..

Как синхронизировать сценарий без транзакций? Штатными средствами Java

15.06.2021 02:14:19 | Автор: admin

Давайте представим, что вы параноик, и параноик вдвойне, когда дело касается многопоточности. Предположим, что вы делаете backend некого функционала приложения, а приложение переодически дергает на вашем серверы какие-то методы. Все вроде хорошо, но есть одно но. Что если ваш функционал напрямую зависит от каких-либо других данных, того же банального профиля например? Встает вопрос, как гарантировать то, что сценарий отработает именно так, как вы планировали и не будет каких-либо сюрпризов? Транзакции? Да это можно использовать, но что если Вы фантастический параноик и уже представляете как к вам на сервер летит 10 запросов к одному методу от разных клиентов и все строго в одно время. А в этот момент бизнес-логика данного метода завязана на 100500 разных данных. Как всем этим управлять? Можно просто синхронизировать метод и все. Но что если летят еще и те запросы, держать которые нет смысла? Тут уже начинаются костыли. Я пару раз уже задавался подобным вопросом, и были интересно, ведь задача до абсурда простая и повседневная (если вы заботитесь о том, чтобы не было логических багов конечно же :). Сегодня решил подумать, как это можно очень просто и без костылей реализовать. И решение вышло буквально на 100 строк кода.

Немного наглядного примера

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

String result = l.lock(new ArrayList<Locker.Item>() {{    add(new Locker.Item(SimpleType.TRIP, 1));    add(new Locker.Item(SimpleType.USER, 2));}}, () -> {    // Тут выполняем отмену поездки и держим водителя на привязи    // Кстати если кто-то где-то вызовет USER=2 (водитель), то он также будет ждать    // ну или кто-то обратится к поездке TRIP=1    // А если обратится к USER=3, то уже все будет нормально :)    // так как никто не блокировал третьего пользователя :)    return "Тут любой результат :)";    });

Элегантно и просто! :)

Исходники тут - https://github.com/GRIDMI/GRIDMI.Sync

Камнями не бросаться! :)

Подробнее..

Запросить 100 серверов нельзя оптимизировать код. Ставим запятую

15.06.2021 20:16:32 | Автор: admin

Можно выделить ряд алгоритмов, которые являются базовыми и лежат в основе практически каждой строчки программ, написанных на языках высокого уровня. Хорошо иметь под руками классический многотомный труд Дональда Кнута "The Art of Computer Programming", там детально разобраны многие базовые алгоритмы. Но прочесть и усвоить все задача, требующая много усилий и времени, которая должна как-то быть мотивирована.


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


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


Является продолжением серии предыдущих публикаций.


Введение


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


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


  • case_id уникальный идентификатор кейса/прецедента;
  • record журнальная запись события в кейсе;
  • start время регистрации.

Используемые библиотеки


library(tidyverse)library(data.table)library(rTRNG)

Наиболее интересной задачей является генерация временных меток. События должны идти последовательно во времени для каждого кейса в отдельности. Сначала подготовим простую "рыбу". В частном случае мы возьмем для демонстрации малое число кейсов. В продуктиве их может быть 10^5-10^n, что определяется задачами.


Пример кода
# определим число кейсовnn <- 100# создаем первичный набор кейсовrecords <- c("first", "one and a half", "second", "third", "fourth",              "fifth", "sixth")# готовим два варианта для экспериментовdf <- tibble(case_id = 1:nn, recs = list(records)) %>%  unnest(recs)dt <- as.data.table(df)[, case_id := as.numeric(case_id)]# указание ключа приводит к физической сортировке данныхsetkey(dt, case_id)head(df, 10)

  # A tibble: 10 x 2     case_id recs                 <int> <chr>            1       1 first            2       1 one and a half   3       1 second           4       1 third            5       1 fourth           6       1 fifth            7       1 sixth            8       2 first            9       2 one and a half  10       2 second  

Теперь приступим к интересному блоку генерации временных меток. Для простоты задачи сведем ее к распределению долей в интервале [0; 1] в рамках каждого кейса. Перевод в реальный unixtimestamp оставим за пределами, это неинтересно. Варианты с явными циклами также за пределами. Времена исполнения приведены на условном компьютере, главное, что выполняется все на одном.


Создание одной временнОй метки


Вариант 1. Прямолинейный


Этот вариант предлагается в большинстве случаев. А что, все просто и понятно.


Пример кода
f1 <- function(df) {  df %>%    group_by(case_id) %>%    mutate(t_idx = sort(runif(n(), 0, 1))) %>%    ungroup()}

Получаем такие условные показатели. Наверное, неплохо. Но не забываем, что тут всего 100 кейсов.


  median `itr/sec` mem_alloc 15.38ms      63.2   284.9KB

Подумаем, что можно улучшить?


Вариант 1+1/2. Прямолинейный + быстрый генератор чисел


Есть хорошая библиотека rTRNG. На больших объемах она дает существенное ускорение, в том числе, за счет параллельной генерации. Просто проверим:


Пример кода
f1_5 <- function(df) {  df %>%    group_by(case_id) %>%    mutate(t_idx = sort(runif_trng(n(), 0, 1))) %>%    ungroup()}

  median `itr/sec` mem_alloc 29.34ms      29.5   284.9KB

На малых объемах не получили никакого выигрыша. Это все? Конечно же нет. Мы знаем, что tidyverse медленнее data.table, попробуем применить его. Но здесь мы попробуем применить первую хитрость отсортировать вектор времен по индексам, а потом его переприсвоить.


Вариант 2. Однопроходный, через индексы data.table


Пример кода
f2 <- function(dt) {  # здесь полагаемся на то, что мы заранее отсортировали уже по `case_id``  # формируем случайные числа и сортируем их по кейсам  vec <- dt[, t_idx := runif_trng(.N, 0, 1)][order(case_id, t_idx), t_idx]  # возвращаем сортированный   dt[, t_idx := vec]}

Получается вполне неплохо, ускорение раз в 15-20 и памяти требуется почти в три раза меньше.


  median `itr/sec` mem_alloc   1.69ms     554.      109KB 

Останавливаемся? А почему да?


Вариант 3. Однопроходный, через композитный индекс


На самом деле, как только мы сваливаемся в цикл, явный, или через by, мы резко просаживаемся в производительности. Попробуем сделать все за один проход. Идея следующая сделать композитный индекс, который позволил бы нам отсортировать все события одним махом. Используем трюк. Поскольку у нас внутри кейса все временные метки будут в диапазоне [0; 1], то мы можем разделить индекс на две части. Целочисленная часть будет содержать case_id, дробная часть временнУю долю. Однократная сортировка одного такого индекса сохранит принадлежность строчек case_id, при этом мы одним махом отсортируем значения внутри каждого кейса


Пример кода
f3 <- function(dt) {  # делаем трюк, формируем композитный индекс из case_id, который является монотонным, и смещением по времени  # поскольку случайные числа генерятся в диапазоне [0, 1], мы их утаскиваем в дробную часть (за запятую)  # сначала просто генерируем случайные числа от 0 до 1 для каждой записи отдельно   # и масштабируем одним вектором  dt[, t_idx := sort(case_id + runif_trng(.N, 0, 1, parallelGrain = 10000L)) - case_id]}

Запускаем и получаем выигрыш еще в 2 раза против предыдущего варианта, как по времени, так и по памяти.


  median `itr/sec` mem_alloc 826.7us    1013.     54.3KB

Вариант 3+1/2. Однопроходный, через композитный индекс, используем set


Останавливаемся? Можно и остановиться, хотя поле для сжатия еще есть. Дело в том, что при таких малых временах исполнения накладные расходы на NSE становятся весьма ощутимыми. Если использовать прямые функции, то можно получить куда лучший результат.


Пример кода
f3_5 <- function(dt) {  set(dt, j = "t_idx",       value = sort(dt$case_id + runif(nrow(dt), 0, 1)) - dt$case_id)}

Ускорение еще в 5 раз, памяти потребляем в 4 раза меньше


  median `itr/sec` mem_alloc 161.5us    5519.     16.3KB

Промежуточное подведение итогов


Соберем все вместе.


Тестируем
bench::mark(  f1(df),  f1_5(df),  f2(dt),  f3(dt),  f3_5(dt),  check = FALSE)

  expression      min   median `itr/sec` mem_alloc  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>1 f1(df)       14.3ms  15.38ms      63.2   284.9KB2 f1_5(df)    24.43ms  29.34ms      29.5   284.9KB3 f2(dt)       1.55ms   1.69ms     554.      109KB4 f3(dt)        722us  826.7us    1013.     54.3KB5 f3_5(dt)    142.5us  161.5us    5519.     16.3KB

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


Создание временнОй метки начала записи и окончания


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


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


Вариант 1. Прямолинейный


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


Пример кода
# Cоздание ЧЕТРЕХ колонок -- case_id, record, start, finish# Все как в предыдущем, только для каждого записи finish > start # и для двух последовательных записей 1, 2 в одном кейсе start_2 > finish_1 dt[, t_idx := NULL] # очистим хвосты предыдущего упражненияf1 <- function(df) {  df %>%    group_by(case_id) %>%    mutate(ts_idx = sort(runif(n(), 0, 1))) %>%    ungroup() %>%    # еще раз пройдемся генератором, используя время начала следующей записи как границу    # чтобы избежать NaN при переходе между кейсами (в случае max < min),     # принудительно выставим порог 1 в таких переходах, NA в последней строке тоже заменим на 1    mutate(tf_idx = {lead(ts_idx, default = 1) %>% if_else(. > ts_idx, ., 1)}) %>%    mutate(tf_idx = map2_dbl(ts_idx, tf_idx, ~runif(1, .x, .y)))}

В целом меньше секунды, но, очевидно, что это ОЧЕНЬ далеко от оптимальности.


  median `itr/sec` mem_alloc  28.16ms      30.7    2.06MB 

Вариант 2. Однопроходный, через композитный индекс и матрицы


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


Пример кода
f2 <- function(dt){  dt[, c("ts_idx", "tf_idx") := {    # используем принцип vector recycling    x <- case_id + runif(2 * .N, 0, 1);    m <- matrix(sort(x), ncol = 2, byrow = TRUE) - case_id;    list(m[, 1], m[, 2])  }]}

В легкую сократили время и память почти в 30 раз! Да и код стал существенно проще и прямолинейнее.


  median `itr/sec` mem_alloc   1.04ms     733.    74.38KB 

Вариант 2+1/2. Однопроходный, через композитный индекс, матрицы и set


Пример кода
f2_5 <- function(dt){  x <- dt$case_id + runif(2 * nrow(dt), 0, 1)  m <- matrix(sort(x), ncol = 2, byrow = TRUE) - dt$case_id  set(dt, j = "ts_idx", value = m[, 1])  set(dt, j = "tf_idx", value = m[, 2])}

Перфекционизм в действии. Еще в 4 раза ускорили.


  median `itr/sec` mem_alloc  278.1us    2781.    57.55KB 

Промежуточное подведение итогов


Соберем все вместе.


Тестируем
bench::mark(  f1(df),  f2(dt),  f2_5(dt),  check = FALSE)

  median `itr/sec` mem_alloc  28.16ms      30.7    2.06MB   1.04ms     733.    74.38KB  278.1us    2781.    57.55KB 

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


Заключение


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


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


Предыдущая публикация Оценка структуры кредитного портфеля с помощью R.

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru