Введение в машинное обучение.
Вводятся понятие классификации на базе библиотек scikit-learn. Рассмотрены как минимум следующие классификаторы: svm (Support Vector Machine, метод опорных векторов) включая ядровый трюк, MLP (Multi-layer Perceptron, Многослойный перцептрон, нейронные сети), SGD (Stochastic Gradient Descent, Стохастический градиентный спуск) и RidgeClassifier (ridge classification, гребневый классификатор). В процессе изложения показано насколько важную роль играют параметры того или иного метода.
Это предварительная версия! Любые замечания приветсвуются.
Задача классификации является частным случаем общей формулировке задачи машинного обучения показаной в самой первой заметке про регрессии. Так, в задаче лкассификации считается, что регрессия строится для конечного, при этом существенно небольшого колличества чисел. Фактически значения образуют дискретное множество или просто конечное множество объектов. Интерпретация такая. По признакам строится прогноз нечисловой, а элементу из множества. Например, по признакам (где обитает, сколько весит в взослом состоянии, сколько ног и так дале) определить животное. Или как в примере из прошлой заметке определить название цветка ириса.
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
%matplotlib inline
Линейные модели как следует из названия предполагают, что данные линейной разделимы -- речь про бинарную классификацию. Так, существует гипер-плоскость такая что по одну сторону будут элементы одного класса, а по другую другого.
from sklearn import linear_model
X = [[0, 0], [1, 1]] # Две точки на двумерной плоскости.
y = [0, 1] # Им соответсвуют данные классы.
Берем формулу из второй заметки, которую мы минимизируем, и считаем, что сама функция принимает два значения6 -1 и 1. 1 для одного класса, а -1 для другого. Ествественно значения не будут идеальными. Например, значению может быть равно 1.1 или 0.8 для класса 1.
Как и ранее ищится минимум данной фкнции. Но теперь вадно обратить внимание на функцию потерь. В зависимости от того какая будет выбрана будет зависеть и качественый результат по сути. А точнее некотрым из фукнций потерь соответвуют те или иные традиционные методы классификации. Так, логорифмическая ункция потерь даст логистическую регрессию.
# Общий метод градиентного скуска.
# Характеристики зависят от функции потерь.
clf = linear_model.SGDClassifier()
clf.fit( X, y)
# Теперь можно предсказывать:
p = [-4,-5] # Проверим класс для данной точки.
clf.predict( [p] ) # В общем случае передает список. Поэтому в квадратных скобках.
# По умолчанию loss равен hinge
clf = linear_model.SGDClassifier(loss='log') # Теперь будет логисчическая регрессия.
clf.fit( X, y)
clf.predict( [[3, 2]] )
Для конкртеных методов есть более оптимальный алгоритм из вычисления.
clf = linear_model.RidgeClassifier()
clf.fit( X, y)
Есть более мощные методы, которые по умолчанию тоже работают как линейные модели, но имеют расширеные верси для работы с кривыми поверхзностями. К таким методам относится, например, метод опроных векторов.
from sklearn import svm # Загружаем базовый модуль.
# Под модули:
# svm -- suport vector machines, метод опорных векторов.
# Содержит разные вариации.
# Отличающиеся в ом числе скоротью работы.
# LinearSVC -- линейный (без ядрового трюка)
clf = svm.LinearSVC() # #SVC() SVC( kernel = 'poly' )
clf.fit(X, y) # Главный метод все моделей это fit.
# т.е. подсроится под данные.
clf.predict( [p] ) # Более првильное значение "регресии", далее определили класс по знаку.
# Значение исходной функции (без отнесение к классу)
clf.decision_function([p])
# строим сетку точек, для которых будет построен прогноз.
# В прошлой заметки это делали более детально.
x = np.linspace(-1, 2, 50)
y = np.linspace(-1, 2, 50)
Xx, Xy = np.meshgrid(x, y)
#Xy.shape, Xx[0].shape
x
Xx.shape # Сетка 50 на 50 точек.
Xx[10, 24] # Х-овая кордината точки с индексом 10, 24.
Xy[10, 24] # Y-овая кордината точки с индексом 10, 24.
p = [1.5, 1.3 ]
ans = clf.decision_function( [p] )
ans[0] # Берем нуденвой элемент из возвращенного списка.
# Создаем массив/матрицу нужного размера из нулей.
# Нужного это под нашу сетку.
Z = np.zeros( (x.shape[0], y.shape[0]) )
for i in range( x.shape[0] ):
for j in range( y.shape[0] ):
#print( [Xx[j][i], Xy[j][i]] ) # Отладочная печать, если надо.
# Заполняем массив:
Z[i,j] = clf.decision_function( [[Xx[j][i], Xy[j][i]]] )
Z
Z.shape # Проверяем что массив имеет нужный размер.
# Хотим построить гистограмму.
# Чтобы например показать, что знчения не сконцентрированы в одном или двух какх то местах...
# И просто увидеть характер их распределения.
plt.hist(Z.flatten()) # flatten напомню превращает двумерный массив/матрицу в одномерный массив/вектор.
# Теперь можно отобразить значение функционала.
plt.scatter( Xx.flatten(), Xy.flatten(), c=Z.flatten() )
Хотим получить тоже самое но по питоновски...без цикла
По сути зочется построить список точек (их координат) сетки.
# Показано что в даном случае методы ravel и flatten делают одно и тоже.
np.mean(Xx.ravel()==Xx.flatten())
# Показано, что метод c_ может взять массивы и создать из них матрицу по-столбцам.
np.c_[[1,2],[-2,-4]]
# Показано, что метод r_ может взять массивы и создать из них матрицу по-строкам.
# Получится объединение строк.
np.r_[[1,2],[-2,-4]]
Xx.ravel()
# Теперь строим список точек. Цикл на не понадобился.
points = np.c_[Xx.ravel(), Xy.ravel()]
points
# Массив имеет нужный размер.
points.shape # Нужное колличество точек из двемерной сетки.
# Напомним как вычислять значение в точке.
clf.decision_function( [[0.25, 0.3 ]] )[0]
clf.predict( [[0.25, 0.3 ]] )[0]
# Теперь создаем массим значений без цикла.
Z = clf.decision_function( np.c_[Xx.ravel(), Xy.ravel()] )
# Обратно формируем матрицу по одномерному массиву значений.
Z = Z.reshape( Xx.shape )
# Покажем что получили такой же ответ.
plt.scatter( Xx.flatten(), Xy.flatten(), c=Z.flatten() )
Бывает важным не цветную картинку рисовать, а строить линии уровня, т.е. показать где данная двумерная фунция принимает конкретные значения.
#plt.plot( Z, c='r')
# Автоматическое построение.
plt.contour(x, y, Z) # На каждом контуре фкнция принимает конкретное значение.
Z = clf.decision_function( np.c_[Xx.ravel(), Xy.ravel()] ) #predict
# Put the result into a color plot
Z = Z.reshape( Xx.shape )
plt.contour(x, y, Z)
Изоражение рукописных цифр имеет фисированный рамер. изображение черное белое (скорее даже градации серого). Изображение как известно харакетезуется его размером: некое колличество пикселов в ширину и в высоту. Фактически изображение является матрицей чисел. Признаками являются сами пикселы, т.е. отдельные элементы из которых изображение состоит. Их общее количество будет ширина умноденная на высоту.
Рассмотрим одну из классическиз задачь... определение цифры по изображению её рукописной заиси. Иначе говоря на вход будет подаватся изображение с рукописной цифрой (от 0 до 9). С точки зрения постановки задачи важно, что ничего друго не может быть там написано быть. т.е. гарантируется что одна из фифр (и только одна) изображена. Так вот по изображению нужно определить цифру.
from sklearn.datasets import load_digits # Данные для цифр
# Загружаем датасет из изображений рукописных чисел.
digits = load_digits()
Смотрим что находится в данных. На основании того что там есть решить отдкуда брать изображения и метки (правильные ответы).
# Смотрим ключи "словаря"
digits.keys()
# Это очевидно метки.
digits['target'] # target стандартное название для них.
# Проверим нашу интуицию.
np.unique(digits['target']) # Тут должны быть только цифры от 0 до 9.
# И ничего больше! (конечно, только для данного датасета)
digits['target'].shape
Это вроде сами изображения. проверим что количество элементов в массиве images совпдает с количеством ответов.
digits['images'].shape
# Видно даже, что размер каждого изображения 8 на 8.
# Отобразим матрицу изображения.
digits['images'][74] # матрица 8 на 8.
Вывдеим матрицы изображения виде картинки для лучшего понимания.
dig = digits['images'][74]
plt.imshow( dig ) # Картинка цветная и вводит в заблуждение.
Посмотрим какой должен быть правильным ответ.
digits['target'][74]
Сделаем все-таки изображения в градациях серого.
img = np.zeros((dig.shape[0], dig.shape[1], 3))
img[:,:,0] = dig
img[:,:,1] = dig
img[:,:,2] = dig
np.max(img), img.shape # Вычислил максимум значения изображения и тип.
# Видими, что изображение имеет всего 16 градаций.
img.dtype
img /= np.max(img) # Отнормируем изображение (так чтобы максимум был равен 1).
plt.imshow( img ) # Теперь выглядит как надо.
В данном массиве по суте хранятся названия для людей. В более сложном датасете они были бы более осмысленными. В даном они совпадабт с самими метками.
digits['target_names']
Для начала попробуем линейную модель
def train( clf, data, labels ):
n = data.shape[0]
perm = np.random.permutation( n )
tr_n = int(0.85 * n)
train = perm[ :tr_n ]
valid = perm[ tr_n: ]
train_dig = data[ train ]
train_lab = labels[ train ]
valid_dig = data[ valid ]
valid_lab = labels[ valid ]
clf.fit( train_dig, train_lab )
val_cnt = sum( clf.predict( valid_dig ) == valid_lab )
return clf.predict( valid_dig ), valid_lab #val_cnt / valid_lab.shape[0]
clf = svm.LinearSVC() # Вариант для линейных моделей.
pred, valid = train( clf, digits['data'], digits['target'] )
qq = pred == valid
sum(qq)/qq.shape[0]
Уже прекрасный результат.
clf = linear_model.SGDClassifier()
pred, valid = train( clf, digits['data'], digits['target'] )
qq = pred == valid
sum(qq)/qq.shape[0]
qq = pred == valid
#q3q
sum(qq)/qq.shape[0]
clf = linear_model.RidgeClassifier()
pred, valid = train( clf, digits['data'], digits['target'] )
qq = pred == valid
sum(qq)/qq.shape[0]
Как ранее было уже указано метод опорных веторов пдддерживает ядровый трюк, т.е. кривые разделяющие поверхности.
clf = svm.SVC( kernel = 'poly', gamma='auto' ) #svm.LinearSVC() # #SVC() SVC( kernel = 'poly' )
clf.fit( digits['data'][:1400], digits['target'][:1400] )
pred = clf.predict( digits['data'][1400:] )
pred[232], digits['target'][1400:][232]
sum( digits['target'][1400:] == pred )
pred.shape
378/397
np.mean( digits['target'][1400:] == pred )
Оценим наше везение
ll = []
# На практике наверное не нужно делать такое большое колличество испытаний.
for k in range( 2000 ):
clf = svm.SVC( kernel = 'poly', gamma='auto' )
tr, vl = train( clf, digits['data'], digits['target'] )
eq = tr == vl
ll.append( np.mean(eq) )
# Скорее делается для того чтобы данных было много.
# Покажем часть.
ll[:10]
# и соответственно, статистики были более точные.
np.mean(ll), np.std(ll)
params = sp.stats.norm.fit( ll )
params
dat=sp.stats.norm.pdf( np.arange(0.965, 1, 0.001 ), loc = params[0], scale = params[1] )
np.arange(0.965, 1, 0.01)
plt.hist( ll, 30, density = True );
plt.plot( np.arange(0.965, 1, 0.001), dat*3 )
clf = svm.SVC( kernel = 'poly', gamma='auto' )
pred, valid = train( clf, digits['data'], digits['target'] )
Зафиксируем какой либо класс. Например конкретную цифру. Два главных понятия:
Разберем синтезированые примеры
Возьмем два вектра чисел. Правильные ответы: [1,1,1,1,2,1], и то что быо предсказано [1,2,1,1,2,2]. Правдивость предсказаний: [Да, Нет, Да, Да, Да,Нет].
Выберем класс, например, 1. Тогда, все места где было предсказано 1, действительно соответсвуют действительности. Значит precision равен 1. Все ли 1 были предсказаны. Нет, не все. Вторая и последняя 1 не были предсказаны (мы сказали что там 2). Значит мы верно предсказали 3 из 5 единиц. 3/5=0.6. Значит recall 0.6.
from sklearn import metrics
print( metrics.classification_report( [1,1,1,1,2,1], [1,2,1,1,2,2] ) )
Упр. По аналогии проверьте класс 2.
Разумеется можно запустить и для большего колличества классов.
print( metrics.classification_report( [1,1,0,1,2,1], [0,2,1,1,2,2] ) )
print( metrics.classification_report( [0,2,2], [1,2,1] ) )
Отчет для обученной модели
print( metrics.classification_report( pred, valid ) )
#clf.s support_vectors_.shape
clf = svm.SVC( gamma = 'auto' )
pred, valid = train( clf, digits['data'], digits['target'] )
qq = pred == valid
sum(qq)/qq.shape[0]
clf = svm.SVC( kernel = 'poly', gamma='auto' )
pred, valid = train( clf, digits['data'], digits['target'] )
qq = pred == valid
sum(qq)/qq.shape[0]
clf = svm.SVC( gamma = 0.001 )
pred, valid = train( clf, digits['data'], digits['target'] )
qq = pred == valid
sum(qq)/qq.shape[0]
Полносвязанные нейронные сети
from sklearn import neural_network
clf = neural_network.MLPClassifier( )
pred, valid = train( clf, digits['data'], digits['target'] )
qq = pred == valid
sum(qq)/qq.shape[0]