Для
эксперимента WordRepresentations делаю последний вариант получения векторного представления слов с помощью автоэнкодеров.
Самая простая модель -
plain vanilla автоэнкодер с архитектурой seq2seq. На входе у него для каждого слова задается цепочка 1-hot представлений символов. Далее цепочка векторов сжимается слоем LSTM элементов в вектор фиксированного размера (32), и LSTM-декодер восстанавливает исходную цепочку векторов символов. Обучив автоэнкодер на нескольких миллионах слов, можно отрезать декодер, оставив первую половину, и прогнать все слова через декодер, получив векторы слов. Тут все стандартно и просто.
Возникает закономерный вопрос: если мы можем подавать на вход
сетки для решения задачи непосредственно 1-hot представления символов, то есть ли смысл сначала обучать автоэнкодер, генерировать представления слов из него и использовать уже эти векторы? Экспериментальная проверка показывает, что смысла нет. Нейросетевой классификатор, использующий 1-hot представления символов, обозначаемый в
таблице результатов как 'char_indeces', дает на 1 миллионе 3-грамм точность около 0.6745, тогда как векторы с автоэнкодера дают только 0.5616.
Попытка улучшить автоэнкодер - встраивание морфологии
Но ничто не мешает нам из вектора на "скрытом" слое автоэнкодера пытаться восстановить еще и другую информацию, в частности - битовый вектор с морфологическими признаками и даже w2v вектор слова. Таким способом можно ожидать, что декодер будет из исходного char surface представления слова получать дополнительную морфологическую и семантическую информацию.
Архитектура сетки получается такая:
На Keras
эта нейросетка описывается так:
input = Input(shape=(self.max_word_len, self.nb_chars), dtype='float32', name='input_layer')
encoder = LSTM( vector_size, input_shape=(self.max_word_len, self.nb_chars), return_sequences=False )(input)
decoder_word = RepeatVector(self.max_word_len)(encoder)
decoder_word = LSTM(vector_size, return_sequences=True)(decoder_word)
decoder_word = TimeDistributed(Dense(self.nb_chars))(decoder_word)
decoder_word = Activation('softmax', name='word')(decoder_word)
decoder_tags = Dense(units=grammar_dict.get_tags_size(), name='tags')(encoder)
decoder_w2v = Dense(units=grammar_dict.get_w2v_size(), activation='tanh', name='w2v')(encoder)
model = Model(inputs=input, outputs=[decoder_word, decoder_tags, decoder_w2v])
model.compile(loss={'word':'categorical_crossentropy', 'tags':'mse', 'w2v':'mse'},
optimizer='rmsprop',
metrics=['accuracy'])
Обращаю внимание, что в качестве loss'ов передается словарь, и для каждого именованного выхода указана своя loss-функция.
Сетка обучается не очень хорошо. На графике показаны значения loss на каждом из выходов сетки, val_word_loss - это ошибка восстановления символов слова, и так далее:
Ошибка восстановления слов периодически скачет вверх, хотя тренд на понижение четко прослеживается.
А вот для ошибки восстановления вектора тегов (val_tags_loss) и w2v вектора (val_w2v_loss) все плохо - уменьшения практически нет. Сначала я подумал, что причина кроется в небольшой величине ошибки val_tags_loss относительно val_word_loss - примерно в 10 раз меньше. То есть градиенты для коррекции декодера слова просто экранируют остальные градиенты.
Чтобы проверить гипотезу, была объявлена модифицированная функция ошибки с масштабированием в 10 раз:
from keras import backend as K
def loss_mse10(y_true, y_pred):
return 10.0*K.mean(K.square(y_pred - y_true), axis=-1)
И она задавалась для сетки для выхода 'tags'.
Результат обучения с такой функцией ошибок:
То есть ошибка успешно отмасштабирована, но обучение по данному аспекту не идет.
Сверточная нейросеть и связь морфологии с морфемами
Может быть, рекуррентная архитектура энкодера выдает плохие представления символьной информации, из которой декодер не может качественно восстановить битовый вектор морфологических тегов? Приверим это предположение, сделав отдельную нейросетку со
сверточной архитектурой. Так как на входе у нас цепочка символов, то
сверточные слои будут фактически выделять
морфемы, от которых по идее есть неплохая корреляция с морфологическими признаками (падеж, род etc). По крайней мере, можно надеяться на это из общих соображений.
Слова имеют разную длину в символах, поэтому надо как-то приводить результаты применения сверток к единому размеру. Удобно делать это с помощью слоя GlobalMaxPooling1D, который ищем максимум для всего входного набора векторов и таким образом отбрасывает информацию о местоположении "морфем", оставляя только факт их наличия.
Делаем сверточную нейросеть (
исходник на питоне для Keras тут):
input = Input(shape=(self.max_word_len, self.nb_chars), dtype='float32', name='input_layer')
nb_filters = 64
conv_list = []
merged_size = 0
for kernel_size in range(2, 6):
conv_layer = Conv1D(filters=nb_filters,
kernel_size=kernel_size,
padding='valid',
activation='relu',
strides=1)(input)
conv_layer = GlobalMaxPooling1D()(conv_layer)
conv_list.append(conv_layer)
merged_size += nb_filters
merged = keras.layers.concatenate(inputs=conv_list)
encoder = Dense(units=int(merged_size/2), activation='relu')(merged)
encoder = Dense(units=int(merged_size/4), activation='relu')(merged)
encoder = Dense(units=vector_size, activation='sigmoid')(encoder)
decoder_tags = Dense(units=grammar_dict.get_tags_size(), activation='relu' )(encoder)
decoder_tags = Dense(units=grammar_dict.get_tags_size(), activation='relu', name='tags')(decoder_tags)
model = Model(inputs=input, outputs=decoder_tags)
model.compile(loss='mse', optimizer='nadam' ) #, metrics=['accuracy'])
Графически это выглядит так:
К сожалению, кроме более быстрого обучения, эта модель не отличается от рекуррентного прототипа с точки зрения проблем обучаемости:
Улучшение на первой эпохе и затем плато с минимальными колебаниями.