Игра в собственные
Имеем набор данных в виде совокупности квадратных матриц, которые используются - вместе с известным выходом - в качестве тренировочного набора для нейронной сети. Можно ли обучить нейронную сеть, используя только собственные значения матриц? Во избежание проблем с комплексными значениями, упор делаем на симметричные матрицы. Для иллюстрации используем набор данных MNIST. Понятно, что невозможно восстановить матрицу по ее собственными значениям - для этого понадобится еще кое-что, о чем мы поговорим далее. Поэтому трудно ожидать некоего прорыва на данном пути, хотя известно, что можно говорить о чем угодно, строить грандиозные планы, пока не пришло время платить. О деньгах мы здесь не говорим, просто задаем глупый вопрос, на который постараемся получить осмысленный ответ, тем более что в процессе познания расширим свои научные горизонты. Например, сначала мы познакомимся с тем, как находить собственные векторы и собственные значения (eigenvalues and eigenvectors) для заданной квадратной матрицы, затем плавно выкатим на эрмитовы и унитарные матрицы. Все иллюстративные примеры сопровождаются простыми кодами. Далее возьмем MNIST , преобразуем в набор собственных значений симметричных матриц и используем молоток от Keras. Как говорят в Японии: “Торчащий гвоздь забивают”. Закроем глаза и начнем бить, а на результат можно и не смотреть: получится как всегда. Сразу скажу, что изложение будет проведено как можно ближе к тому, как я это дело понимаю для себя, не обращаясь к строгому обоснованию, которое обычно не используется в повседневной жизни. Иными словами, что понятно одному глупцу, понятно и другому. Все мы невежественны, но, к счастью, не в одинаковой степени. С другой стороны, предполагаю, что многие, хоть и в гимназиях не обучались, но имеют представление - по своему опыту обучения, - что значит впихнуть невпихуемое.
Собственно, собственные
Итак, пусть у нас задана произвольная квадратная матрица
где
где, вспоминая правила умножения матриц (
где звездочка обозначает обычное комплексное сопряжение. Следует заметить, что
Все это выглядит не очень хорошо, поскольку произведение разных собственных векторов, например
import numpy as np
from numpy import matrix
from numpy.linalg import eig
# из субмодуля (submodule linalg) линейной алгебры берем метод
#для вычисления собственных значений и векторов
# Возьмем матрицу, которую можно проверить вручную
A = matrix([[3, -0.65], [5, 1]])
# calculate eigendecomposition
values, V = eig(A)
print(values) # [2.+1.5j 2.-1.5j]
print(V) # [[0.18814417+0.28221626j 0.18814417-0.28221626j]
# [0.94072087+0.j 0.94072087-0.j ]]
Действительно, характеристическое уравнение
# Первый собственный вектор - все элементы нулевого столбца и т.д.
v0=V[:,0]
v1=V[:,1]
v0.shape, v1.shape # ((2, 1), (2, 1))
# К счастью, они единичные
np.dot(v0.H,v0) # matrix([[1.+0.j]])
np.dot(v1.H,v1)[0,0] # (0.9999999999999999+0j). Ну, почти единица
# Но эти векторы не ортогональны
np.dot(v0.H,v1) # matrix([[0.84070796-0.10619469j]])
Эрмитовы матрицы
Мы научились находить eigendecomposition для произвольной квадратной матрицы. Пока не будем говорить о вырожденных матрицах , которых, по возможности, будем в дальнейшем избегать. Итак, в чем состоит загвоздка. С собственными значениями - все в порядке, а вот собственные векторы изрядно подгуляли. Дело в том, что они не ортогональны друг другу. Посмотрим, какие условия на матрицу нам потребуется наложить, чтобы добиться ортогональности, заодно откроем для себя ряд новых возможностей в деле познания собственных способностей.
Пусть из всего набора мы произвольно выделили два участника
Далее выполним комплексное сопряжение второго уравнения в (5)
Поскольку индексы в последнем уравнении “немые” (по ним производится суммирование) , их можно назвать как угодно. Таким образом, после замены
где
Пока мы не накладывали никаких ограничений на матрицу. Первое, что приходит на ум: положим
Итак, для эрмитовых матриц получаем выражение
Теперь соорудим эрмитову матрицу. Для этого сгенерируем пару случайных матриц
Пришло время окунуться в мутные воды искусственного интеллекта. Внести, так сказать, свой слабый голос в общий хор.
X=np.random.randint(0, 256, (28,28))# Вспоминаем молодость с MNIST
Y=np.random.randint(0, 256, (28,28))
A=matrix(X+1j*Y)
B=A+A.H
# выводим собственные значения и собственные вектора (в виде матрицы)
values, V = eig(B)
values=np.real(values)# только действительная часть
values
Собственные значения (values) получились с едва заметной мнимой частью, которую мы отрезаем по идеологическим соображениям. Для этого мы оставили только действительную часть. Напомним, что
где
np.dot(V.H, V) # почти единичная матрица с формой (28,28)
Опять же получим почти единичную матрицу, но ничего округлять не будем. Теперь пришло время для громкого заявления : матрица, для которой выполняется (8) - унитарная матрица . Таким образом, эрмитово-сопряжённая унитарная матрица совпадает с обратной. К слову, эволюция во времени нашего мира осуществляется с помощью унитарных преобразований, хотя этот факт теряется во множестве событий. Не удивительно, что унитарную эволюцию попытались использовать (см. статью) и в машинном обучении, но для рекуррентной нейронной сети. Для многих концепция унитарности заставляет ощутить священный трепет познания. Но это одна среда обитания. В политическом же питательном бульоне тоже есть концепция унитарности, о которой можно думать все что угодно, и которая не связана ни с каким познанием.
Симметричный MNIST
Итак, в предыдущем разделе мы говорили о произвольных о эрмитовых матрицах. Если же мы имеем дело с действительными матрицами, комплексное сопряжение выпадает, поэтому эрмитовы - это просто симметричные матрицы,
В этом разделе мы возьмем набор данных MNIST и попробуем поработать с каждым элементом как с матрицей. Каждое рукописное изображение цифр (от 0 до 9) - матрица с формой (28,28), каждый элемент которой - число от 0 до 255 (8-битная шкала серого). Загрузим и посмотрим (рекомендую начать новый блокнот).
import keras
from keras.datasets import mnist
keras.__version__ # '2.4.3'
(x,y),(tx,ty)=mnist.load_data()
База данных MNIST состоит тренировочного
import matplotlib.pyplot as plt
# Сколько их?
x.shape[0], tx.shape[0] # (60000, 10000)
# Возьмем произвольный элемент
y[12345] # 3
# Посмотрим на каракули
plt.imshow(x[12345], cmap=plt.get_cmap('gray'))
plt.show()
Все выглядит замечательно, так что можно продолжать работать с нашим изображением. Тем не менее, проблемы вылазят тогда, когда мы посмотрим на матрицу, скрывающуюся под этим изображением.
x[12345]
Сразу можно отметить (посмотрите сами), что слишком много нулей. Это вырожденная матрица, определитель ее равен нулю.
import numpy as np
from numpy.linalg import eig
np.linalg.det(x[12345])# 0.0 - детерминант
Если попробуем вычислить собственные значения, никакого сбоя не случится, просто некоторые из них будут равны нулю.
values, V = eig(x[12345])
values # array([ 0. +0.j , 0. +0.j ,
1474.46266075 +0.j , 838.19932232 +0.j ,
-400.18280429+384.94168117j, -400.18280429-384.94168117j,
297.89145581 +0.j , 90.51616055+144.4741198j ,
90.51616055-144.4741198j , -57.05178416 +68.44040035j,
-57.05178416 -68.44040035j, 3.05833828 +80.86506799j,
3.05833828 -80.86506799j, -32.62937655 +32.08539788j,
-32.62937655 -32.08539788j, 4.36245913 +51.20712637j,
4.36245913 -51.20712637j, -16.0035565 +0.j ,
6.13140522 +0.j , 59.17272648 +0.j ,
0. +0.j , 0. +0.j ,
0. +0.j , 0. +0.j ,
0. +0.j , 0. +0.j ,
0. +0.j , 0. +0.j ])
Как видно, собственные значения находятся в комплексной области. Монтировать комплексную нейронную сеть можно, но осторожно. Основы и ссылки можно подчерпнуть из диссертации. Это отличная тема для исследований и экспериментов; советую использовать только аналитические функции, несмотря на ограничения, связанные с принципом максимума модуля (это для тех, кто в теме).
Итак, хочется поскорее избавиться от комплексных чисел. Мы знаем, что эрмитовы матрицы обладают действительными собственными значениями. Поскольку матрица изображения полностью состоит из действительных чисел, то эрмитова матрица - просто симметричная матрица. Иными словами, нам потребуется соорудить симметричную матрицу, но перед этим избавиться от множества нулей. Для этого “закрасим” пикселями, выбирая их значения случайным образом.
from numpy import matrix
a=matrix(x[12345]+np.random.randint(0, 5, (28,28)))
np.linalg.det(a) # У меня -3.7667039553765456e+43,
# у вас, конечно, получится другое значение
Пятерочки для закраски вполне достаточно, поскольку вероятность совпадения элементов в строке или столбце просто мизерная. Детерминант, конечно, громадный, но Python все стерпит, а пока закроем на это глаза. Если посмотрим на картинку, то поверьте, там все нормально: наша тройка практически не изменилась.
Итак, перейдем к симметричной матрице и посмотрим, что там нарисовано.
a=a+a.H # для действительных матриц a.H=a.T
plt.imshow(a, cmap=plt.get_cmap('gray'))
plt.show()
Кто скажет, что это не тройка, пусть первый бросит в меня камень! Теперь это число подготовлено для манипуляций в нейронной сети. Матрица симметричная, невырожденная, имеет замечательный набор собственных значений.
values, V = eig(a)
values # array([ 4.06238155e+03, 1.87437218e+03, -1.59889723e+03, -8.51935039e+02,
5.58412899e+02, -3.53262264e+02, -2.36036195e+02, 2.29960516e+02,
2.08312088e+02, -1.49974962e+02, 1.40811760e+02, -9.71188401e+01,
8.27836555e+01, -6.51480016e+01, -4.65323368e+01, 3.90299000e+01,
3.06552503e+01, 2.06100661e+01, 1.73972579e+01, -1.16491885e+01,
-8.13295407e+00, 1.24942529e+01, -4.67339958e+00, 9.33795587e+00,
6.67620627e+00, 2.96767577e+00, 1.13851798e+00, 1.86752842e-02])
Если с собственными значениями все в порядке, то собственные векторы малость подгуляли: мало того, что они комплексные (как обычно и бывает), так восстановление первоначальной матрицы по собственным векторам и собственным значениям может привести к комплексной матрице. Python не знает, что это не правильно, поэтому и вытянет наружу комплексные остатки. Само собой, можно от них избавиться с помощью метода np.real(), оставляя только действительную часть. Но пока попробуем в лоб, проверим прозорливость создателей NumPy на примере восстановления матрицы по собственным значениям и собственным векторам. Чтобы подготовить почву, необходимо вернуться к уравнениям (8), которые справедливы и для нашей матрицы
где
в котором использовали (8) . Теперь проверим в коде
e=np.diag(values) # сооружаем диагональную матрицу из собственных значений
b=np.dot(V,np.dot(e,V.H)) # восстанавливаем исходную матрицу по собственным
# значениям и векторам; ради приличия ввели новое
# имя матрицы
plt.imshow(b, cmap=plt.get_cmap('gray'))
plt.show()
Нет слов, все работает! Итак, наши подозрения были беспочвенны, но проверять все равно надо. Теперь мы знаем, что по собственным значениям и векторам можно четко восстановить исходную матрицу. Итак, если используем обратимые операции, царапая формулы на листке бумаге, лучше проверить, как с этим обстоит дело в коде.
Эксперимент
Итак, теперь мы умеем манипулировать симметричными матрицами, используя в качестве полигона базу данных MNIST. Теперь мы попытаемся представить набор данных как симметричные матрицы, затем вычислить собственные значения, на основе которых построить нейронную сеть по примеру обращения с обычным набором. Сразу скажу, что надежд на удачный исход мало. Действительно, как мы выяснили ранее, для того чтобы восстановить матрицу собственных значений недостаточно, для этого необходим набор собственных векторов, объединенных в матрицу
Начинаем новый блокнот:
import numpy as np
from numpy.linalg import eig
import keras
from keras.datasets import mnist
(x,y),(tx,ty)=mnist.load_data()
Теперь напишем функцию, которая будет “закрашивать” матрицы из набора данных. Отмечу, что использую доморощенные функции, которые предпочитаю писать самостоятельно, чем искать на мутных форумах.
def paintSym(z):
n=z.shape[0]
x=np.empty([n,28,28])
for i in range(n):
x[i]= z[i]+np.random.randint(0, 5, (28,28))
return x
Используем эту функцию, затем масштабируем (это не обязательно).
x=paintSym(x)
x=x.astype('float32') / 260
На следующем шаге нам понадобится функция, которая делает симметричные матрицы и возвращает только собственные значения.
def eigSym(z):
n=z.shape[0]
x=np.empty([n,28])
for i in range(n):
val, vec = eig((z[i]+z[i].T)/2)
x[i]= val
return x
Включаем и смотрим.
x=eigSym(x)
x[12345] # на всякий случай
# array([ 7.82546663e+00, 3.60277987e+00, -3.06879640e+00, -1.62636960e+00,
1.07726467e+00, -6.77050233e-01, -4.54729229e-01, 4.45481062e-01,
4.00029063e-01, -2.85397410e-01, 2.71449655e-01, -1.94830164e-01,
1.41906023e-01, -1.15398742e-01, -8.69008303e-02, 7.79602975e-02,
5.81430644e-02, -3.25826630e-02, 3.82260419e-02, 3.11152730e-02,
1.78121049e-02, -1.12444209e-02, -6.83658011e-03, 8.95012729e-03,
8.10696371e-03, 3.64418700e-03, -2.08872650e-03, 4.40481490e-05])
x.shape # (60000, 28)
Как видно, вполне приемлемые значения, с которыми можно и поработать. Для этого возьмем продвинутый молоток от Keras.
from keras import models
from keras import layers
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28,)))
network.add(layers.Dense(10, activation='softmax'))
network.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
from keras.utils import to_categorical
y = to_categorical(y)
network.fit(x, y, epochs=100, batch_size=128)
За основу мы берем код из Глубокое обучение на Python. Если осмысленно побалуетесь с цифрами, что-то получите. Точность, которая едва-едва заваливает за 0,5 (за 100 эпох!!!), подкачала по сравнению с оригиналом (accuracy: 0.9897 за Epoch 5), когда используется полная матрица изображения, но хотя бы мы не множили сущности. Для обсуждения не требуется глубокомысленных обобщений, просто, как я наивно думал, если зайца долго бить по голове, он научится спички зажигать. Но оказалось, что этот принцип в глубоком обучении не всегда срабатывает. Возиться дальше - интересно, но бессмысленно. Самое главное, по пути к этому мутному результату можно много чему научиться.
Пару слов напоследок
Итак, пришло время подводить итоги. Понятно, что говорить пока не о чем. Мы попытались на собственных значениях матриц построить работающую схему. Не получилось. Но известно, что отрицательный результат - куда более значимое событие, чем положительный, поскольку при этом у нас открываются новые перспективы. Если бы у нас все получилось, настало бы время для рутинной работы, а так - продолжим идти дальше. Правда и ложь - два взгляда на одну и ту же реальность. Но мы, по крайней мере, не лгали. Просто посмотрели с другого ракурса, заодно открыли для себя новые возможности. К примеру, почему бы не найти преобразование, позволяющее связать все изображения одного класса без использования внешнего наблюдателя для подтверждения события? Иными словами, из одной цифры получить почти полную совокупность из данного набора данных; из “тройки” почти все “тройки”, и т.д.. Сразу приходит на ум конформное отображение двумерных поверхностей: локальные вращения и растяжения с сохранением углов между кривыми, а значит с сохранением формы бесконечно малых фигур. Заодно мы могли бы проследить за трансформацией собственных значений, дабы по их расположению выйти на классификацию по классам. И не обязательно использовать действительные собственные значения. Конечно, переход в комплексную область потребует дополнительных усилий, но там уже существует путь в один путь, многое успешно отработано.
Что касается реальных, на настоящий момент, дел, то это классификация собственных значений по примеру распределения уровней сложных систем. Опять же MNIST животворящий. Но это тема следующей публикации.