Получение sparse distributed representation для слов с помощью автоэнкодера

Nov 17, 2016 17:58

Ранее ( 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

нейросети, vector space model, sparse autoencoder, sparse distributed representation, deep learning, autoencoder, keras, character language model, vector model, word embedding

Previous post Next post
Up