Ранее (
http://kelijah.livejournal.com/197813.html) я описывал алгоритм получения
SDR из word2vec векторов через факторизацию матрицы.
Далее опишу более прямой и проще модифицируемый способ получения SDR для слов с помощью deep learning автоэнкодера.
Часть 1. Сеточная модель
На входе алгоритма будут цепочки символов, представляющие валидные слова. Их можно без проблем собрать из большого текстового корпуса. А я просто взял список словоформ из SQL словаря. В любом случае, мы будем использовать небольшую часть всех возможных комбинаций символов, поэтому имеем право ожидать от модели, что она найдет какие-то интересные статистические закономерности.
Слова приводим к нижнему регистру, оставляем только те, в которых 32 кириллические буквы.
Определяем максимальную длину слова, у меня получилось 29 символов.
Каждое слово будем представлять как длинный вектор битов - 29 кусочков по 32 бита. Все биты нулевые, кроме тех, которые соответствуют символам. Если слово короче 29 символов, то пустые символы оставляют соответствующие блоки полностью нулевыми.
Далее, строим сеточную модель с помощью
Keras. Это будет автоэнкодер, который на вход берет битовый вектор слова длиной чуть меньше 1000 разрядов, что-то с ним делает, и в качестве выходного вектора использует этот же самый вектор.
Сам по себе входной вектор, полученный из преобразования символов в 1-hot представление, является сильно разреженным. Но мы пойдем глубже - пусть из него получается sparse вектор длиной 4096.
Чтобы автоэнкодер занимался чем-то полезным, а не просто копировал входы на выходы, добавим в модель
Drop-out и наложим достаточно сильную
регуляризацию на активность sparse-слоя. На самом деле, как нетрудно убедиться, без дропаута модель вообще отказывается сходиться к какому-то результату, так что это не просто усилитель алгоритмического вкуса, а базовый компонент блюда.
model = Sequential()
model.add( Dense( input_dim=input_size, output_dim=128, activation='relu' ) )
model.add( Dropout(0.1) )
model.add( Dense( output_dim=SPARSE_SIZE, activation='sigmoid', activity_regularizer=regularizers.activity_l1(1e-7) ) )
model.add( Dropout(0.01) )
model.add( Dense( output_dim=output_size, activation='sigmoid' ) )
model.compile(loss='mse', optimizer='rmsprop')
Первый афинный слой, кстати, тоже важен. Как показано на кусочке исходника, я ставлю для него
ReLU активацию. Если задать
sigmoid, то модель перестает сходится или будет сходиться очень медленно! И что интереснее, без этого начального слоя модель тоже отказывается работать. В общем, входной слой как-то перемешивает исходный вектор, и уже эта активность поступает на широченный слой с regularizers.activity_l1.
Величина регуляризации очень сильно влияет на количество единичных битов на выходе слоя, поэтому ее надо подбирать вручную. Например, указанное значение 1e-7 приводит к тому, что гистограмма активности на этом слое имеет такой вид:
histo=[ 9.715 0.029 0.018 0.013 0.012 0.011 0.011 0.012 0.016 0.163 ]
В этих 10 интервалах накоплено относительное кол-во разрядов от 0 до 1. Видно, что использованная L1 регуляризация вынуждает разряды иметь значения либо около 0 (таких накоплено 9.715), либо около 1 (0.163). Уменьшение activity_regularizer будет приводить к тому, что доля единичных разрядов будет расти, а нулевых - уменьшаться, а промежуточные значения будут по-прежнему редки.
Часть 2. Визуализация
После некоторого количества эпох обучения на миллионе слов модель стабилизируется - ошибка восстановления входного вектора перестает уменьшаться и гистограмма активности на sparse-слое перестает меняться.
Теперь возникает второй вопрос - что, собственно, делает автоэнкодер с исходным вектором слова?
Для сверточных сеток, работающих с изображениями, придуманы
разные способы визуализации происходящих на скрытых слоях процессов. Для текстовых данных придумать аналог не так-то просто, в силу крайней дискретности материала.
В целом, задачу можно сформулировать так. Нас интересует, что надо подать на вход модели, чтобы на sparse слое некий разряд принял значение 1, а остальные разряды остались 0.
Я сначала попробовал использовать оптимизационный подход. Рассматриваем вектор слова, подаваемый на вход модели, как свободную переменной, значение которой мы подбираем, чтобы оптимизировать оценочную функцию. В качестве оценочной функции берем евклидово расстояние между активностью на sparse слое и 1-hot вектором, в котором единственный единичный разряд соответствует нужному.
Этот подход, однако, не заработал. По всей видимости, зависимость оценочной функции от входного вектора в таком многомерном пространстве (таки размерность в 1000 великовата!) настолько сильно изобилует локальными минимумами, что градиентный спуск не способен ничего сделать.
Поэтому был опробован второй подход. Набор слов, на которых обучалась модель, нарезаем на шинглы - цепочки символов по 1...4 символа с разных позиций слова. Получается около полумиллиона разных шинглов, если учитывать начальную позицию цепочки в слове. Далее, каждый шингл считаем входным словом, то есть подаем его вектор на вход модели. Смотрим активность на выходе sparse-слоя. Сделав это для всех шинглов, можем отсортировать шинглы по активности интересующего разряда и напечатать top-10 лучшых шинглов. В итоге получим 4096 наборов по 10 шинглов. Эти наборы должны показать какую-то корреляцию между входными частями слов и разрядами SDR.
Получается вот такая штука (далее top-10 лучших шинглов для некоторых разрядов в SDR).
Иногда четко видны корреляции с парой символов:
sparse bit #1
зате --> dist=5.99984338423 bit=1.0
абан --> dist=6.35433429868 bit=1.0
бань --> dist=6.56108760509 bit=1.0
йане --> dist=6.68065117625 bit=1.0
шане --> dist=6.71414022856 bit=1.0
ваче --> dist=6.73194107601 bit=1.0
зане --> dist=6.90341709459 bit=1.0
шате --> dist=6.91943180887 bit=1.0
ызне --> dist=7.18688183608 bit=1.0
вань --> dist=7.1895918083 bit=1.0
Или так:
sparse bit #2
коцу --> dist=10.6228062821 bit=0.999526083469
клай --> dist=10.0624009173 bit=0.999409794807
крой --> dist=9.56251912028 bit=0.999394774437
вуай --> dist=10.5085730819 bit=0.999361097813
куцу --> dist=10.5194808416 bit=0.999356091022
азой --> dist=11.5924327292 bit=0.999325454235
алой --> dist=11.0477046337 bit=0.999212026596
взой --> dist=10.55505118 bit=0.999177515507
коул --> dist=12.8998473139 bit=0.998905420303
цере --> dist=7.55837048252 bit=0.998848438263
Иногда видно, что разряд активируется при наличии одного из двух символов в разных позициях слова, например "т" и "р":
sparse bit #8
ляти --> dist=16.2157744467 bit=1.0
лята --> dist=17.0978813926 bit=1.0
итик --> dist=20.4211268968 bit=1.0
таем --> dist=21.1132596519 bit=1.0
тика --> dist=21.2346503211 bit=1.0
ржде --> dist=23.1428299375 bit=1.0
игур --> dist=23.355822994 bit=1.0
ирур --> dist=23.5305242446 bit=1.0
ьгир --> dist=23.7158076728 bit=1.0
кард --> dist=23.7376820857 bit=1.0
И в таком духе 4096 разрядов.
Исходник на питоне лежит по ссылке:
http://www.solarix.ru/for_developers/download/polygon/word2word/word2word_sparse.py