Введение в машинное обучение.
Вводятся базовые элементы питона (Python версии 3.xx): численные выражения, переменные, циклы, списки и наборы.
Это предварительная версия! Любые замечания приветсвуются.
Код программы предствляется в виде блоков, содержащиш некий код с пояснениями. Предполягается постепенное построение программы, т.е. не сразу все пишется, а за несколько этапов. Промежуточный результат выводится и на основании его пишется новый блок.
Далее идет пример блока (с коментарием).
# Замечания, пояснения в коде начинаются с символа решетка и идут до конца строки. Как, например, эта строка.
# Код может быть разбит на блоки. Это например новый блок кода. При выполнеии они нумеруются.
# Можно выолнять вычисления как на калькуляторе. Доступны стандартные операции: +, -, * и /.
# Для этого выражение записывается в блоке
2.0 + 3.5 * ( 5 - 3.)
# и наживается клавиша воспроизведения (shift-enter).
# Отмечу, что последнее значение блока выводится в качестве результата блока.
5/2 # Обычное деление.
# Также доступны вычислеия над целыми числами:
# целочисленное деление
5//2
(-5)//2 # Действует правило округление "вниз" (как и для положительных чисел) -- берется нижняя граница.
# При деление было бы -2.5. Внизу -3.
# Скорее даже это не округление, а взятие целой части.
# Можно несколько выражений одновременно вычислять.
5//3, 5//-3
# Можно также вычислить остаток от деления:
5%2
Напомню, что если $r$ остаток от деления $a$ на $b$, то имеет место представление: \begin{equation*} a=r + k*b, \end{equation*} где $k$ некое целое число. Также должно быть выполнено: \begin{equation*} 0 \leq r < b. \end{equation*} При этом пока считается, что $b > 0$. В согласии с данным:
7 % 5 # Это должно быть понятно.
# 7 = 2 + 1 * 5
(-7) % 5 # Но и это: остаток всегда будет положительным.
# -7 = 3 + (-2) * 5
(-5) % (-3) # Ну только, если сам делитель положительный. Иначе справедливо следующее.
# -5 = -2 + 1 * (-3).
В том случае отрицательного делителя ($b < 0$), остаток, считается, тоже должен быть отрицательным, т.е. \begin{equation*} -b < r \leq 0. \end{equation*} Последнее связано с тем, что делитель определяет класс эквивалентности между целыми числами. Операция остаток отображает произвольное число на его класс.
5 % (-3) # Поэтому в данном случае остаток будет также как раз отрицательным.
# 5 = -1 + (-2) * (-3).
2.5 % 2.1 # Можно даже так.
# Возведение в степень задается двойной звездочкой, т.е. 3 в степени 2 записывается как:
3 ** 2
# Степень конечно может быть и дробной:
32 ** (1/5) # т.е. корень пятой степени из 32.
# Проверим:
2.0 ** 5
Упр. Найди корни квадратного уравнения.
С каждым выражением связан его тип. В зависимости от него могут вариьероваться поведение других чстей программы.
type( 5 ) # Тип выражения с целым числом.
type( 5. ) # Тип выражения с вещественным числом.
Для расширения возможностей ествественно необходимо применять сторонний код, а именно библиотеки. В следующем блоке показано как это делается (вызываются библиотечные функции). Одна из самых известных математических библиотек в питоне -- numpy: это библиотека для работы с векторами, матрицами. Она же применяется для вычисления стандартных функций: тригонометрических, экспонента, логарифм, степень и др.
# Для использования гтовых библиотек достаточно их загрузить в среду питона:
import numpy # Загрузили библиотек numpy.
# Теперь можно вычислить например:
2.2 * numpy.sin( 1.1 )
Следует отметить то как функция вызывается: не сама по себе, а через префикс "npumpy.". Последнее связано с тем, что функция sin из библиотеки numpy. Последнее сделано чтобы функции из разных библиотек не перепутались: не перекрыли друг друга при одинаковом названии.
Но, название numpy длинное. Есть расширение даннной команды для загрузки библиотеки и присвоение ей сокращенного имени.
# Данная строчка загружает библиотеку numpy и дает ей сокращенное название np.
import numpy as np
Теперь можно использовать сокращенное название np вместо полного numpy.
2.2 * np.sin( 1.1 ) # Применили функцию синус (np.sin).
Используется префикс np так как она была загружена в систему с сокращенным названием np. Поэтому такое обозначение.
# Ествественно в качестве аргумента функции можно исользовать не только число, но и целое выражение
np.sqrt( 4**2 + 3**2 )
# Если вдруг не понятно, на всякий случай упомину и случай функция от фукнции:
np.arcsin( np.sin(0.5) )
# Доступны и константы
np.pi # Например, число пи.
np.e # e
Упр. Реши уравнение sin(ax+b)=c.
Главная особенность понятия кодинга/программирования это наличие переменных -- сущность позволяющая сохранить значение вычисления. Например, в них можно сохранить предыдущий результат вычисления. Более того, переменной значение можно переприсвоить (сохранить новое значение).
a = 2. - 5 * np.cos( np.pi ) # Вычисляем очередное выражение. Но главное в другом. Значение сохранается в переменную.
Переменная как коробка с именем. В неё можно положить что-то и оно там останется пока не будет положено что-то новое. Приэтом у коробки есть имя, т.е. коробок много и все с своим уникальным именем.
Результат предыдущего блока не вывелся по причине того, что он был сохранен в переменную.
a # Можем вывести значение переменной явно, т.е. фактически значение которое туда было ранее помещено.
Переменная хранит некое значение, а значит с ней связан и тип (хранимого значения): целое, вещественное число, строчка и тому подобное.
# В питоне можно узнать какой тип у переменной так:
type( a ) # Оно выводится из типа выражения значение которого было присвоено.
Отмечу, что в других языках сначала объявляется переменная нужного типа, а потом присваивается ей значение. Иначе не получиться.
В питоне иначе. Переменная создается (если нужно) в момент присвоения ей значения. Последнее имеет как свои преимущества, так и недостатки. Преимущество в том, что не нужно отдельно создавать переменную (это лишний код), тем более, что обычно тип переменой можно вывести из самого выражения. Недостаток заключается в том, что система не будет следить за правильностью присвоения значения переменной, т.е. переменной, которая ранее хранила матрицу, можно присовить просто число, а то и вообще строку некого текста.
Итого, в переменую сохранаяется как значение, так и её тип.
type( 5 )
Главное, что можно выполнять вычисления с переменными. В данном случае будет использоваться ранее сохраненное значение в переменной. Вместо переменной в выражение будет подставлено её значение и выражение вычислиться как обычно.
a * 2 - 10 # Привычислении используется зачение переменной a.
a * 3 + 5; # Точка с запятой блокирует вывод вычисления. В данном случае получается, что данной строчка бессмысленна.
Если попытаться использовать не существующую переменную, то компилятор будет ругаться.
5 + n # В данном случае он укажет, что переменая n не задана.
Не менее важно и, что переменной можно, ествественно, присвоить новое значение выражения. Если переменная ранее не существовала, то она создается. Если она уже существовала, то она забывает свое прошлое значение, и получает новое.
a = 3 + 1 # Придает переменной a новое значение, старое забывается.
a #Это строчка чтобы вывод все-ткаи увидеть значение переменной a.
bb = a * 2 # Переменных ествественно может быть много. Их имена могут быть длинными, но с некими ограничениями.
bb # Но не будем об этом.
Одну и туже переменную конечно можно использовать и справа и слево от равенста. Важно понимать: сначала вычисляется зачение правой части, потом данное значение присваивается левой, где подразумевается переменная (иначе будет ошибка).
a = a + 1 # В правой части используеться переменная a, которой и присваиваеться значение.
a
В левой части должна быть переменная.
a + 1 = a + 2 # В левой части не должно быть выражения. Ну разве что какого-то хитрого...
# Иначе будет ошибка.
Блок можно выполнять нескоко раз, для этого его нужно щелкнуть и заново нажать кнопку проигрывателя (или нажать shift-enter). Попробуй это сделать с блоком содержащем строчку "a = a + 1" При каждом новом выполнении значение блока будет менятся.
# Отмечу, что при присвоении переменная забывает не только свое значение, но и свой тип.
# Например, сейчас тип у переменной a целочисленый.
a
# Это определяется отсутвием точки (запятой). Но можно это проверить и явно спросив это у системы:
type( a )
# Но если перейти к вычисления с действительными числами, то тип сразу изменится.
a = 25 ** 0.5 # хотя в даном случае значение будет тем же. Но это чистое совпадение.
a
# Повторим запрос типа у системы.
type( a )
# Итого, в первом случае тип был целочисленым (int), а во втором вещественный (плавающая точка, float)
Важно понимать, что система как в таких простых случаях, так и в более сложных ругатся не будет. Она такое присвоение со сменой типа разрешит (во многих других языках так нельзя). Это важно, когда появяться более сложные выражения.
Подчеркну ещё раз, что в переменную записывается значение выражения, а не само выражение (как формула), которое это значение выдало. Покажу это на характерном прмере.
a = 5 # Присвоим переменной a значение,
b = a * 2 # переменной b -- значение выражения.
a, b # Выведем значения переменных. Сразу обе, через запятую. На самом деле это набор (tuple)
a = -3 # Зададим новое значение для переменной a.
b # Изменится ли значение перменной b? Конечно же нет!
# Прежде чем переходить к самому главному объекту языка Python покажу как можно присваивать значения в параллель.
a, b = 5 + bb, 6 - np.sin ( np.pi/2 )
# По аналогии чтобы вывести значения переменных пишем:
b, a # Специально в другом порядке.
# Убедись, что ты понимаешь результат.
Упр. Поменяй значения у двух переменных: имеем две переменные (например, a и b), нужно сделать так, чтобы значение переменной a было рано прошлому значению переменной b, а переменная b была равна прошлому значению переменной a. Подсказка... традиционно для этого используют ещё одну переменную. Если хочешь усложнить упражнение, то реши без дополнительной переменной.
Вычисления над отдельными числами это хорошо, но нам понадобиться список чисел. Хотябы чтобы хранить наши данные (котировки акций, список товаров и тому подобное). Список позволяет зранить несколько значений, в общем случае, разных типов (как если бы они были разными перемеными). Главное, что список подддерживает добавление новых элементов (в его конец), а также извлечение и удаление подсписков.
[4, 8, 15, 16, 23, 42] # Создаем список из 4 элементов.
[ 2 + 5, 7 - 9] # В момент создания список можно проинициализровать выражениями.
Чтобы были понятны принципы работы с ним, нужно его все-ткаи присвоить переменной.
l = [4, 8., 15, 16, 23, 42] # Присвоили переменной l списк чисел. Внимание на второе число!
type( l ) # Выведем его тип. Видно что он отличается от ранее используемых числовых типов.
К списку можно относится как проиндексированныйм (пронумерованным) переменным.
l[3] # Число в квадратных скобках указывает на индекс в списке. Внимание, элементы списка нумеруются с 0.
l[2] = 1 # Можно присловить другое значение элементу списка. Отсылка к прониндексированым переменным.
l # Выводим значение переменной l, т.е. списк чисел
type( l[3] ) # Но, тип самого элемента списка ествественно свой, числовой.
type( l[1] ), l[1] # Тип у разных элементах списка может быть разным.
len( l ) # Можно узнать размер списка.
l[7] = 2 # Индекс должен соответствовать существующему элементу списка. Иначе будет ошибка.
l[3:5] # Можно указывать диапазон индексов: от и до. Причем, "до" невключительно, т.е. в данном случае индексы: 3 и 4.
l[3:100] # При задании интервала индесов диапазон может выходить за пределы списка.
# В таком случае диапазон будет укорочен. Ошибкой это считаться не будет.
l[2:] # Если после двоеточия числа нет, то значит нужно идти до конца списка.
l[:4] # Если числа нет до двоеточия, то значит новый список начинется с начала данного.
# Итого, если не указывать явно одну из границ диапазона индексов, то берутся по масимому.
l[:5], l[3:] # т.е. илибо с начала либо до конца.
l[1:100:2] # В расширенном варианте можно указать шаг увеличения индекса. В данном случае он равен 2.
l[1::2] # Ествественно в расширенном варинте тоже можно опускать числа. Смысл будет прежний.
Упр. Как из списка извлечть два подсписка элементы которых поочередно содержать элементы из первоначального списка?
# Отмечу, что элементы списка можно нумировать и с конца. Для этого используются отрицательные индексы.
l[-1] # -1 соответствует первому элементу с конца, т.е. последнему.
l[-2] # Второй элемент с конца.
# Такие элементы также можно использовать при задании диапазона индексов.
l[:-2] # В данном случае, извлекутся все элементу кроме двух посдних.
l[::-1] # Шаг тоже может быть отрицательный. В даном случае получится список в обратном порядке.
Упр. Как переставить в обратном порядке элементы списка с четным индексом?
# Присвоить можно и диапазону значений сразу:
l[1:3] = 3, 7 # Список чисел должен соответствовать количеству (слева).
l
l.append(3) # Список на то и списк, что к нему можно добавить элемент в его конец.
l
len( l )
del l[2:5] # Удаляет элемент с данным индексом (ами)
l
del l[1], l[1] # Удаляем один или более разрозненных элементов. Подумай почему индекс указан ожин и тот же.
l
l.append(5)
l.append(3)
l.append(8)
l
l.remove(3) # Вырезаем (удаляем) первый элемент из списка равные данному.
l
l.append( -5 )
l.append( 7 )
l
Списки нужны для хранения данных. Если мы захотим все числа умножить на два, то возможно мы бы написали:
l * 2 # Хотим умножить все элементы списка на два.
И увидели бы результат, который скоре всего не соответсвует нашим ожиданиям. Дело в том, что список является объектом сам по себе. Поэтому он имеет свои смысловые нагрузки на операции. Например, + объединяет списки (конкатенация), точнее она ставит один список (второй) в конец другому (первому).
[1, -5] + [-7, 5, 18] # Контакенация, один список добавили в конец к другому. Получили новый список
l + [2] # как append добавили 2 в конец списка.
l # Операция выполнена, но не сохранена. Поэтому старно значение l.
# Операции умножение соответствует многократной конкатенации (т.е. повтору) самого себя:
[2, 5] * 3
l * 2. # Только для целых чисел. Для плавающей точки будет даже ошиюка.
Упр. Как циклически сдвинуть элементы списка? Последнее подразумевает, что все элементы сдвинутся на один вперед, а первый попадет в конец.
Возвращаясь к нашему желанию выполнить опредленное действо для всех элементов. Для это используются циклы: for. Сама конструкция является генератором списка, т.е. она по списку строит список (того же размера!).
l # Напоним значение.
[ x * 2 for x in l] # По умному это назвается map, т.е. применение отображения к каждому из элементов списка.
[ x * 2. for x in l] # У всех элементов теперь точка, т.е. они теперь все имеют тип float.
x * 2 for x in l # Так нельзя. Обрати внимание на квадратные скобки в примере в прошлом блоке.
Другим действием является редукция,т.е. сведение списка к некому значению. Но прежде чем углубляться в эту компактную конструкцию рассмотрим дла начала её более полный вариант.
Например, нам нужно вычислить сумму элементов списка.
summa = 0 # Обявили переменную summa для хранения суммы всех элементов списка. Присваиваем первоначальное значение.
# Конструкция for позволяет взять каждый элемент списка поочередно и совершить некое действие над ним.
# В данном случае элементы берутся их списка l и временно сохраняются в переменную x.
for x in l: #l -- мы ранее определяли. Двоеточее в конце строки обазательно.
# Отступ необходим.
summa = summa + x # Каждый элемент добавляется к общей сумме. Операция делается постепенно.
summa # По умному это назвается reduce, т.е. редукция, когда список сводится к "числу".
# Список, ествественно, можно задавать разными способами. Например явным. Временная переменная тоже необязательно x.
summa = 0
cnt = 0
# Цикл пробегает по элементам явно заданного списка.
for d in [2, -5, 9, 4]: # В даном примере действо для каждого элемента будет состоять из двух строчек.
summa = summa + d # По аналогии считается сумма чисел.
cnt = cnt + 1 # Но и их общее количество. Это позволит вычислить среднее значение.
summa/cnt # Выводим среднее значение элементов списка.
[ x for x in l if x%2 == 1 ]
# Помимо map и reduce важной операцией над списками является операция filter, т.е. фильтрация объектов по некому условию.
[x for x in [1, -4, 7, 2, 9] if x%2 == 0] # Создает список состоящей их четных чисел.
Логическое выражение
10 > 5
(10 > 5) and ( 111 < 200 ) # and, or, not
(10 < 5) or (3 < 5)
(10 < 5) or (3 > 5)
Обратно к циклам
# Сущесвуют ряд встроеных объектов, позволяющих упростить процесс создания списков.
[i for i in range(20)] # Списки можно создавать циклом. Используется range объект задающий диапазон.
# В данном случае с 0 до 20 (не включительно).
Упр. Как по списку построить список пар из текущего и следующего элемента? Из текущего и два последующих элемента?
# В заключении обсуждения списков отмечу, что их элементы не обязаны иметь один и тот же тип
ll = [2, "строка текста", 6]
ll
# Элемент с индексом 0 и 2 содержит
ll[0], ll[2]
# А элемент с индексом 1 являтся строчкой:
ll[1]
type( ll[1] )
'строка текста'[4] # Строка текста тоже на самом деле может быть проиндексирована.
ll[1][5] # Извлекаем отдельную букву в этом списке.
type(ll[0]), type(ll[2])
# Последнее показывает, что списки могут соедржать совершенно разнородные объекты. В этом смысле они полноценные.
# Поэтому, естественно, что списки могут содержать списки.
a = [ 2, 3, [4, 5], 8] # Список из трех элементов.
a
# Нулевой, первый и третьий содержат числа:
a[0], a[1], a[3]
# А вот второй элемент сам является списком:
a[2]
# Один из способов, как вычислить значение от каждого элемента списка, было показано выше.
# Показанный способ (отображение) был явный. Он был реализован на чистом питоне без вспомогательных средств.
# Но можно вычислить функцию от каждого элемента списка инче, использую вспомогательные (библиотечные) функции.
a = np.sin( [1.1, 2.2, -4.4] )
a
# Полученый объект уже не будет списоком (см. слово array). Он преврятится в массив, т.е. array.
Тип array с точки зрения обычных языков фактически является массивом, т.е. объектом у которого тип элементов постоянен. По суте идексируемая переменная (имеет тот же тип). Такой тип соответсвует математическим понятиям вектора, матрице. Переменая с индексом. В следующей заметке перейдем к описанию массивов.
Мы уже ранее его видели. Всякий раз когда выводятся числа в скобках -- это он. Схож со список, но имет свои отличия. Скорее такой тип данных следует считать набором, т.е. фиксированного размера объект.
a = (0, 4)
a
Предполагается, что компоненты будут иметь определеный смысл.
a = ('Шокуров', 'Антон', 'Вячеславович')
a
a[1] # Можно индексировать.
a[1] = 'антон' # Присвовить новое значение нельзя.
Изменять состав нельзя:
a.append('aa')
a + ['aa']
a + ('aa',) # А так можно. Мы создали новый набор.
del a[1]
Упр. Как поменять значения двух переменных по питоновски? Как циклически сдвинуть значения? В последем имеется ввиду, что у нас есть переменные a, b и c. Нужно сделать так, чтобы их значения сместились циклически, т.е. после преобразования значение a равнялось прошлому значению переменной b, b -- c, а c -- a.