Меню

Ядро матрицы и как его найти это



Что такое ядро матрицы и как его найти?

Как найти ядро (нуль-пространство) матрицы?
С помощью команды kernel.
Вспомним определение ядра (нуль-пространством) матрицы:
Ядро матрицы А – это множество векторов х таких, произведение матрицы А на которые равно нулевому вектору. Поиск ядра матрицы А эквивалентен решению системы линейных однородных уравнений.
Ядром матрицы A являются все решения уравнения AW=0.

Синтаксис команды kernel
W=kernel(A [,tol,[,flag])
Параметры
A : действительная или комплексная матрица. В случае, если матрица представлена в разреженном виде (тип sparse), то только действительная A.
flag : строка, имеющая значение ‘svd’ (по умолчанию) или ‘qr’
tol : действительное число
W : полная матрицы

Команда kernel возвращает ортономальный базис нуль-пространства матрицы A. W=kernel(A) возвращает ядро матрицы A. Если матрица W будет непустой, будет выполняться:
A*W=0 .

Параметры flag и tol являются необязательными: flag = ‘qr’ или ‘svd’ (по умолчанию принимает значение ‘svd’). Указывают на используемый алгоритм вычисления.

tol = параметр допуска. В качестве значения tol по умолчанию принимается величина порядка %eps(

Пример.
A=[2 -3 5 7;4 -6 2 3;2 -3 -11 -15]

A =
! 2. — 3. 5. 7. !
! 4. — 6. 2. 3. !
! 2. — 3. — 11. — 15. !

W =
! .3026009 .7751569 !
! .2244353 .5075518 !
! — 7491452 .3042427 !
! .5448329 — .2212674 !

! .0444089 .0222045 !
! .0666134 .0666134 !
! .1776357 .0888178 !

Видно, что s является матрицей с практически нулевыми элементами.

Источник статьи: http://otvet.mail.ru/question/37008622

Ядро и образ линейного отображения

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

Образом линейного отображения называется множество образов всех векторов из . Образ отображения обозначается или

Заметим, что символ следует отличать от — мнимой части комплексного числа.

Примеры ядер и образов линейных отображений

1. Ядром нулевого отображения является все пространство , а образом служит один нулевой вектор, т.е.

2. Рассмотрим отображение , которое ставит в соответствие каждому вектору n-мерного линейного пространства его координатный столбец относительно заданного базиса . Ядром этого отображения является нулевой вектор пространства , поскольку только этот вектор имеет нулевой координатный столбец . Образ преобразования совпадает со всем пространством , так как это преобразование сюръективно (любой столбец из является координатным столбцом некоторого вектора пространства ).

3. Рассмотрим отображение , которое каждому вектору n-мерного евклидова пространства ставит в соответствие алгебраическое значение его проекции на направление, задаваемое единичным вектором . Ядром этого преобразования является ортогональное дополнение — множество векторов, ортогональных . Образом является все множество действительных чисел .

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

Свойства ядра и образа линейного отображения

1. Ядро любого линейного отображения является подпространством: .

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

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

Следовательно, множество является линейным подпространством пространства .

2. Образ любого линейного отображения является подпространством: .

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

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

Дефектом линейного отображения называется размерность его ядра: , а рангом линейного отображения — размерность его образа: .

3. Ранг линейного отображения равен рангу его матрицы (определенной относительно любых базисов).

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

4. Линейное отображение инъективно тогда и только тогда, когда , другими словами, когда дефект отображения равен нулю: .

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

5. Линейное отображение сюръективно тогда и только тогда, когда , другими словами, когда ранг отображения равен размерности пространства образов: .

6. Линейное отображение биективно (значит, обратимо) тогда и только тогда, когда и одновременно.

Теорема (9.1) о размерностях ядра и образа. Сумма размерностей ядра и образа любого линейного отображения равна размерности пространства прообразов:

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

Во-первых, , так как образ любого вектора линейно выражается через векторы

Во-вторых, образующие линейно независимы. Если их линейная комбинация равна нулевому вектору:

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

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

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

Действительно, для обратимости преобразования (см. свойство 6) его матрица (размеров ) должна удовлетворять условиям (см. свойства 3,4,5):

Тогда по теореме 9.1 заключаем, что , т.е. матрица — квадратная n-го порядка и невырожденная , что и требовалось доказать.

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

Источник статьи: http://mathhelpplanet.com/static.php?p=yadro-i-obraz-linyeinogo-otobrazheniya

Смотрим на Chapel, D, Julia на задаче вычисления ядра матрицы

Введение

Кажется, стоит вам отвернуться, и появляется новый язык программирования, нацеленный на решение некоторого специфического набора задач. Увеличение количества языков программирования и данных глубоко взаимосвязано, и растущий спрос на вычисления в области «Data Science» является связанным феноменом. В области научных вычислений языки программирования Chapel, D и Julia являются весьма релевантными. Они возникли в связи с различными потребностями и ориентированы на различные группы проблем: Chapel фокусируется на параллелизме данных на отдельных многоядерных машинах и на больших кластерах; D изначально разрабатывался как более продуктивная и безопасная альтернатива C++; Julia разрабатывалась для технических и научных вычислений и была нацелена на освоение преимуществ обоих миров — высокой производительности и безопасности статических языков программирования и гибкости динамических языков программирования. Тем не менее, все они подчеркивают производительность как отличительную особенность. В этой статье мы рассмотрим, как различается их производительность при вычислении ядра матрицы, и представим подходы к оптимизации производительности и другие особенности языков, связанные с удобством использования.

Вычисление ядра матрицы формирует основу методов в приложениях машинного обучения. Задача достаточно плохо масштабируется -O(m n^2), где n — количество векторов, а m — количество элементов в каждом векторе. В наших упражнениях m будет постоянным и мы будем смотреть на время выполнения в каждой реализации по мере увеличения n. Здесь m = 784 и n = 1k, 5k, 10k, 20k, 30k, каждое вычисление выполняется три раза и берется среднее значение. Мы запрещаем любое использование BLAS и допускаем использование только пакетов или модулей из стандартной библиотеки каждого языка, хотя в случае D эталон еще сравнивается с вычислениями, использующими Mir, библиотеку для работы с многомерными массивами, чтобы убедиться, что моя реализация матрицы отражает истинную производительность D. Подробности вычисления ядра матрицы и основных функций приведены здесь.

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

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

Бенчмарки языков программирования на задаче вычисления ядра матрицы

Приведенная выше диаграмма (сгенерированная с помощью ggplot2 на R с помощью скрипта) показывает время выполнения для количества элементов n для Chapel, D, и Julia, для девяти вычислений ядра. D лучше всего работает в пяти из девяти случаев, Julia лучше в двух из девяти, а в двух задачах (Dot и Gaussian) картинка смешанная. Chapel был самым медленным для всех рассмотренных задач.

Стоит отметить, что математические функции, используемые в D, были взяты из math API языка C, доступного в D через core.stdc.math, так как математические функции в стандартной библиотеке std.math языка D бывают достаточно медленными. Использованные математические функции приведены здесь.

Для сравнения рассмотрим скрипт mathdemo.d, сравнивающий C-функцию логарифма с D-функцией из std.math:

Объект Matrix, используемый в бенчмарке D, был реализован специально из-за запрета на использование модулей вне стандартных языковых библиотек. Чтобы удостовериться, что эта реализация конкурентоспособна, т.е. не представляет собой плохую реализацию на D, я ее сравниваю с библиотекой Mir’s ndslice, тоже написанной на D. На диаграмме ниже показано время вычисления матрицы минус время реализации ndslice; отрицательное значение означает, что ndslice работает медленнее, что указывает на то, что используемая здесь реализация не представляет собой негативную оценку производительности D.

Условия тестирования

Код был выполнен на компьютере с операционной системой Ubuntu 20.04, 32 ГБ памяти и процессором Intel Core i9-8950HK @ 2.90GHz с 6-ю ядрами и 12-ю потоками.

Компиляция

Julia (компиляция не требуется, но может быть запущена из командной строки):

Реализации

Chapel

Chapel использует цикл forall для распараллеливания по потокам. Также используется C-указатели на каждый элемент, а не стандартное обращение к массивам, и применяется guided итерация по индексам:

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

Для распараллеливания кода D используется taskPool потоков из пакета std.parallel. Код на D претерпел наименьшее количество изменений для оптимизации производительности — большая польза от использования специфического компилятора и выбранных ключей компиляции (обсуждается далее). Моя реализация Matrix позволяет отобрать столбцы по ссылке с помощью refColumnSelect.

Julia

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

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

Макросы @ bounds и @ simd в основных функциях использовались для отключения проверки границ и применения оптимизации SIMD к вычислениям:

Эти оптимизации дают достаточно заметный прирост, но очень просты в применении.

Использование памяти

Суммарное время для каждого бенчмарка и общая используемая память была собрана с помощью команды /usr/bin/time -v. Вывод для каждого из языков приведен ниже.

Chapel занял наибольшее общее время, но использовал наименьший объем памяти (почти 6 Гб оперативной памяти):

D расходует наибольший объем памяти (около 20 ГБ оперативной памяти на пике), но занимает меньше общего времени, чем Chapel для выполнения:

Julia потратила умеренный объем памяти (около 7,5 Гб пиковой памяти), но выполнялась быстрее всех, вероятно, потому что ее генератор случайных чисел является самым быстрым:

Оптимизация производительности

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

  • Статическая диспетчеризация функций ядра вместо использования полиморфизма. Это означает, что при передаче функции ядра используется параметрический (времени компиляции) полиморфизм, а не динамический (времени исполнения), при котором диспетчеризация с виртуальными функциями влечет за собой накладные расходы.
  • Использование представлений/ссылок, вместо копирования данных в многопоточном режиме, имеет большое значение.
  • Распараллеливание вычислений имеет огромное значение.
  • Знание того, что массив является основным для строки/столбца, и использование этого в вычислениях имеет огромное значение.
  • Проверки границ и оптимизации компилятора дают огромную разницу, особенно в Chapel и D.
  • Включение SIMD в D и Julia внесло свой вклад в производительность. В D это было сделано с помощью флага -mcpu=native, а в Julia это было сделано с помощью макроса @ simd.

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

Код на D изменился очень мало, и большая часть производительности была получена за счет выбора компилятора и его флагов оптимизации. Компилятор LDC богат возможностями оптимизации производительности. Он имеет 8 -O уровней оптимизации, но некоторые из них повторяются. Например, -O, -O3 и -O5 идентичны, а других флагов, влияющих на производительность, бесчисленное множество. В данном случае использовались флаги -O5 —boundscheck=off -ffast-math, представляющие собой агрессивные оптимизации компилятора, проверку границ, и LLVM’s fast-math, и -mcpu=native для включения инструкций векторизации.

В Julia макросы в рассмотренных ранее изменениях заметно улучшили производительность, но они не были слишком запутанными. Я попробовал изменить уровень оптимизации -O, но это не улучшило производительность.

Качество жизни

В этом разделе рассматриваются относительные плюсы и минусы, связанные с удобством и простотой использования каждого языка. Люди недооценивают усилия, затрачиваемые на повседневное использование языка; необходима значительная поддержка и инфраструктура, поэтому стоит сравнить различные аспекты каждого языка. Читателям, стремящимся избежать TLDR, следует прокрутить до конца данного раздела до таблицы, в которой сравниваются обсуждаемые здесь особенности языка. Было сделано все возможное, чтобы быть как можно более объективным, но сравнение языков программирования является сложным, предвзятым и спорным, поэтому читайте этот раздел с учетом этого. Некоторые рассматриваемые элементы, такие как массивы, рассматриваются с точки зрения «Data Science»/технических/научных вычислений, а другие являются более общими.

Интерактивность

Программистам нужен быстрый цикл кодирования/компиляции/результата во время разработки, чтобы быстро наблюдать за результатами и выводами для того, чтобы двигаться вперёд либо вносить необходимые изменения. Интерпретатор Julia — самый лучший для этого и предлагает гладкую и многофункциональную разработку, а D близок к этому. Этот цикл кодирования/компиляции/результата может быть медленным даже при компиляции небольшого кода. В D есть три компилятора: стандартный компилятор DMD, LLVM-компилятор LDC и GCC-компилятор GDC. В этом процессе разработки использовались компиляторы DMD и LDC. DMD компилирует очень быстро, что очень удобно для разработки. А LDC отлично справляется с созданием быстрого кода. Компилятор Chapel очень медленный по сравнению с ним. В качестве примера запустим Linux time для компилятора DMD и для Chapel для нашего кода матрицы без оптимизаций. Это дает нам для D:

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

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

Документация и примеры

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

Документация Julia наиболее близка по качеству к документации на Python и даёт пользователю очень плавный, детальный и относительно безболезненный переход на язык. Она также имеет богатую экосистему блогов, и темы по многим аспектам языка легкодоступны. Официальная документация D не так хороша и может быть сложной и разочаровывающей, однако существует очень хорошая бесплатная книга «Программирование на D», которая является отличным введением в язык, но ни одна единичная книга не может охватить язык программирования целиком и не так много исходных текстов примеров для продвинутых тем. Документация Chapel достаточно хороша для того, чтобы сделать что-то, хотя представленные примеры различаются по наличию и качеству. Часто программисту требуется знать, где искать. Хорошая тема для сравнения — библиотеки файлового ввода/вывода в Chapel, D и Julia. Библиотека ввода/вывода Chapel содержит слишком мало примеров, но относительно ясна и проста; ввод/вывод D распределён по нескольким модулям, и документации более сложно следовать; документация по вводу/выводу Julia содержит много примеров, и она ясна и проста для понимания.

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

Поддержка многомерных массивов

«Массивы» здесь относятся не к массивам в стиле С и С++, доступным в D, а к математическим массивам. Julia и Chapel поставляются с поддержкой массивов, а D нет, но у него есть библиотека «Мир», которая содержит многомерные массивы (ndslice). В реализации расчета ядра матрицы я написал свой объект матрицы в D, что несложно, если понимать принцип, но это не то, что хочет делать пользователь. Тем не менее, в D есть линейная библиотека алгебры Lubeck, которая обладает впечатляющими характеристиками производительности и интерфейсами со всеми обычными реализациями BLAS. Массивы Julia, безусловно, самые простые и знакомые. Массивы Chapel сложнее для начального уровня, чем массивы Julia, но они спроектированы для запуска на одноядерных, многоядерных системах и компьютерных кластерах с использованием единого или очень похожего кода, что является хорошей уникальной точкой притяжения.

Мощность языка

Поскольку Julia — это динамический язык программирования, некоторые могут сказать: «Ну, Julia — это динамический язык, который гораздо более разрешительный, чем статические языки программирования, поэтому дебаты закончены», но все гораздо сложнее. В статических системах типов есть свое могущество. У Julia есть система типов, похожая по своей природе на системы типов из статических языков, так что вы можете писать код так, как если бы вы использовали статический язык, но вы можете делать вещи, зарезервированные только для динамических языков. Она имеет высокоразвитый синтаксис общего и мета-программирования, а также мощные макросы. Она также имеет очень гибкую объектную систему и множественную диспетчеризацию. Это сочетание возможностей делает Julia самым мощным языком из трех.

D был задуман как замена C++ и взял очень много от C++ (а также заимствовал из Java), но делает шаблонное программирование и вычисления времени компиляции (CTFE) намного более удобными для пользователя, чем в C++. Это язык с одиночной диспетчеризацией (хотя есть пакет с мультиметодами). Вместо макросов в D есть «mixin» для строк и шаблонов, которые служат аналогичной цели.

Chapel имеет поддержку дженериков и зарождающуюся поддержку для ООП с одиночной диспетчеризацией, в нем нет поддержки макросов, и в этих вопросах он ещё не так зрел, как D или Julia.

Конкурентность и параллельное программирование

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

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

Julia имеет хорошую поддержку как конкурентности, так и параллелизма.

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

Стандартная библиотека

Насколько хороша стандартная библиотека всех трех языков в целом? Какие задачи она позволяют пользователям легко выполнять? Это сложный вопрос, потому что при этом учитываются качество библиотеки и фактор документирования. Все три языка имеют очень хорошие стандартные библиотеки. В D самая полная стандартная библиотека, но Julia — отличная вторая, потом Chapel, но все никогда не бывает так просто. Например, пользователь, желающий написать бинарный ввод/вывод, может найти Julia самой простой для начинающего; она имеет самый простой, понятный интерфейс и документацию, за ней следует Chapel, а затем D. Хотя в моей реализации программы для чтения IDX-файлов, ввод/вывод D был самым быстрым, но зато код Julia было легко написать для случаев, недоступных на двух других языках.

Менеджеры и экосистема пакетов

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

Интеграция с Cи

Cи- интероперабельность проста в использовании на всех трех языках; Chapel имеет хорошую документацию, но не так популярен, как другие. Документация на языке D лучше, а документация на языке Julia — самая полная. Однако, как ни странно, ни в одной документации по языкам нет команд, необходимых для компиляции вашего собственного кода на C и его интеграции с языком, что является недосмотром, особенно когда дело касается новичков. Тем не менее, в D и Julia легко искать и найти примеры процесса компиляции.

Сообщество

Chapel D Julia
Компиляция/ Интерактивность Медленная Быстрая Лучшая
Документация & Примеры Детальные Лоскутные Лучшие
Многомерные массивы Да Только родные
(библиотека)
Да
Мощность языка Хорошая Отличная Лучшая
Конкурентность & Параллелизм Отличная Отличная Хорошая
Стандартная библиотека Хорошая Отличная Отличная
Пакетный менеджер & Экосистема Зарождающаяся Лучшая Отличная
Си -Интеграция Отличная Отличная Отличная
Сообщество Маленькое Энергичное Наибольшее

Таблица характеристик качества жизни для Chapel, D & Julia

Резюме

Если вы начинающий программист, пишущий числовые алгоритмы и выполняющий научные вычисления, и хотите быстрого и простого в использовании языка, Julia — это ваш лучший выбор. Если вы опытный программист, работающий в той же области, Julia все еще является отличным вариантом. Если вы хотите более традиционный, «промышленный», статически скомпилированный, высокопроизводительный язык со всеми «свистоперделками», но хотите что-то более продуктивное, безопасное и менее болезненное, чем C++, то D — это ваш лучший вариант. Вы можете написать «что угодно» в D и получить отличную производительность благодаря его компиляторам. Если вам нужно, чтобы вычисления массивов происходили на кластерах, то Chapel, наверное, самое удобное решение.

С точки зрения грубой производительности в этой задаче, D был победителем, явно демонстрируя лучшую производительность в 5 из 9 эталонных задач. Исследование показало, что ярлык Julia как высокопроизводительного языка — это нечто большее, чем просто шумиха — он обладает собственными достоинствами в сравнении с высококонкурентными языками. Было сложнее, чем ожидалось, получить конкурентоспособную производительность от Chapel — команде Chapel потребовалось много исследований, чтобы придумать текущее решение. Тем не менее, по мере того, как язык Chapel взрослеет, мы сможем увидеть дальнейшее улучшение.

Источник статьи: http://habr.com/ru/post/508004/


Adblock
detector