Вложения слов обеспечивают плотные представления слов и их относительных значений, они являются улучшением по сравнению с разреженными представлениями, используемыми в представлениях простой модели мешков, а вложения слов можно изучать из текстовых данных и повторно использовать в проектах. Их также можно изучить как часть нейронной сети, которая соответствует текстовым данным.
Word Embedding
Вложения слов — это класс методов, которые используют плотные векторные представления для представления слов и документов.
Встраивание слов — это усовершенствование традиционной схемы кодирования модели «мешок слов». Традиционные методы используют большие и разреженные векторы для представления каждого слова или оценивают каждое слово в векторе для представления всего словаря. Эти представления являются разреженными, поскольку представление каждого слово огромно, данное слово или документ представлены главным образом большим вектором нулевых значений.
Напротив, во вложениях слова представлены плотными векторами, где векторное представление проецирует слова в непрерывное векторное пространство.
Позиции слов в векторном пространстве узнаются из текста и основаны на словах, которые окружают слово при его использовании.
Положение слова в выученном векторном пространстве называется его вложением: Вложение.
Два популярных примера методов изучения встраивания слов из текста включают:
- Word2Vec.
- GloVe.
В дополнение к этим хорошо разработанным методам обучение с встраиванием слов также может использоваться как часть моделей глубокого обучения. Это может быть более медленный метод, но таким образом можно настроить модель для определенного набора данных.
Keras Embedding Layer
Keras предоставляет слой для внедрения нейронных сетей в текстовые данные.
Он требует, чтобы входные данные были закодированы целым числом, поэтому каждое слово представлено уникальным целым числом. Этот шаг подготовки данных можно выполнить с помощью API Tokenizer, предоставляемого Keras.
Слой встраивания инициализируется со случайными весами и будет изучать вложения для всех слов в обучающем наборе данных.
Это гибкий слой, который можно использовать по-разному, например:
- Его можно использовать отдельно для изучения встраивания слов, которое впоследствии можно сохранить и использовать в другой модели.
- Его можно использовать как часть модели глубокого обучения, где вложения изучаются вместе с самой моделью.
- Его можно использовать для загрузки предварительно обученных моделей встраивания слов, что является типом трансферного обучения.
Слой внедрения определяется как первый скрытый слой сети. Он должен указать 3 параметра:
- input_dim: это количество возможных значений словаря в текстовых данных. Например, если ваши данные представляют собой целые числа, закодированные как значения от 0 до 9, то размер словаря составляет 10 слов;
- output_dim: это размер векторного пространства, в которое встроены слова. Он определяет размер выходного вектора этого слоя для каждого слова. Например, это может быть 32 или 100 или даже больше, и его можно считать гиперпараметром для конкретной проблемы;
- input_length: это длина входной последовательности, как вы определяете ее для любого входного слоя модели Keras, то есть количество слов, которые принимает вход. Например, если все ваши входные документы состоят из 1000 слов, то input_length равно 1000.
Например, ниже мы определяем слой внедрения со словарем 200 (например, слова, закодированные целыми числами от 0 до 199 включительно), 32-мерное векторное пространство, в которое будут встраиваться слова, и входной документ, каждое слово. 50 слов.
e = Embedding(input_dim=200, output_dim=32, input_length=50)
Слои встраивания имеют свои собственные изученные веса, и если вы сохраните модель в файл, веса слоя встраивания будут включены.
Выход слоя встраивания представляет собой двумерный вектор, по одному для каждого слова, встроенного в последовательность входного текста (входные документы).
Если вы хотите подключить слой Dense непосредственно за слоем Embedding, вы должны сначала использовать слой Flatten, чтобы сгладить выходную 2D-матрицу слоя Embedding в одномерный вектор.
Теперь давайте посмотрим, как мы можем использовать встраивание слоев на практике.
Примеры обучения встраиванию
В этом разделе мы рассмотрим, как изучать встраивания слов, подгоняя нейронную сеть к задаче классификации текста.
Мы определим небольшую задачу, в которой у нас есть 10 текстовых документов, в каждом из которых есть обзор работы, представленной студентом. Каждый текстовый документ классифицируется как положительный «1» или отрицательный «0». Это простая задача анализа настроений.
Сначала мы определим документы и их метки категорий.
# define documents 定义文档
docs = ['Well done!',
'Good work',
'Great effort',
'nice work',
'Excellent!',
'Weak',
'Poor effort!',
'not good',
'poor work',
'Could have done better.']
# define class labels 定义分类标签
labels = [1,1,1,1,1,0,0,0,0,0]
Далее, давайте целочисленно закодируем каждый файл. Это означает, что на входе слой внедрения будет иметь последовательность целых чисел. Мы можем попробовать другие, более сложные модели набора слов, такие как подсчет или TF-IDF.
Предоставлено Керасомone_hot()функция для создания хэша каждого слова в виде допустимой целочисленной кодировки. Мы используем предполагаемый размер словаря 50, что значительно снижает вероятность коллизии хеш-функции.
# integer encode the documents 独热编码
vocab_size = 50
encoded_docs = [one_hot(d, vocab_size) for d in docs]
print(encoded_docs)
[[6, 16], [42, 24], [2, 17], [42, 24], [18], [17], [22, 17], [27, 42], [22, 24], [49, 46, 16, 34]]
Таким образом, более поздние последовательности имеют разную длину, но Керас предпочитает векторизацию входных данных, и все входные данные имеют одинаковую длину. Мы будем дополнять все входные последовательности длины 4, опять же, мы можем использовать встроенные функции Keras (в данном случаеpad_sequences()функция), чтобы сделать это,
# pad documents to a max length of 4 words 将不足长度的用0填充为长度4
max_length = 4
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
print(padded_docs)
[[ 6 16 0 0]
[42 24 0 0]
[ 2 17 0 0]
[42 24 0 0]
[18 0 0 0]
[17 0 0 0]
[22 17 0 0]
[27 42 0 0]
[22 24 0 0]
[49 46 16 34]]
Теперь мы готовы определить наш слой внедрения как часть нашей модели нейронной сети.
При размере словаря 50 и длине входных данных 4 мы выберем 8-мерное пространство вложения.
Модель представляет собой простую модель бинарной классификации. Важно отметить, что на выходе слоя внедрения будут 4 вектора по 8 измерений в каждом, по одному для каждого слова. Мы накладываем его на вектор из 32 элементов, чтобы перейти к плотному выходному слою.
# define the model 定义模型
model = Sequential()
model.add(Embedding(vocab_size, 8, input_length=max_length))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
# compile the model 编译
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
# summarize the model 打印模型信息
print(model.summary())
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (None, 4, 8) 400
_________________________________________________________________
flatten_1 (Flatten) (None, 32) 0
_________________________________________________________________
dense_1 (Dense) (None, 1) 33
=================================================================
Total params: 433
Trainable params: 433
Non-trainable params: 0
_________________________________________________________________
Наконец, мы можем подобрать и оценить модель классификации.
# fit the model 拟合
model.fit(padded_docs, labels, epochs=50, verbose=0)
# evaluate the model 评估
loss, accuracy = model.evaluate(padded_docs, labels, verbose=0)
print('Accuracy: %f' % (accuracy*100))
Accuracy: 100.000000
Ниже приведен полный код, где мы переписываем определение модели с функциональным API, но структура точно такая же, как и выше.
from keras.layers import Dense, Flatten, Input
from keras.layers.embeddings import Embedding
from keras.models import Model
from keras.preprocessing.sequence import pad_sequences
from keras.preprocessing.text import one_hot
# define documents
docs = ['Well done!',
'Good work',
'Great effort',
'nice work',
'Excellent!',
'Weak',
'Poor effort!',
'not good',
'poor work',
'Could have done better.']
# define class labels
labels = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
# integer encode the documents
vocab_size = 50
encoded_docs = [one_hot(d, vocab_size) for d in docs]
print(encoded_docs)
# pad documents to a max length of 4 words
max_length = 4
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
print(padded_docs)
# define the model
input = Input(shape=(4, ))
x = Embedding(vocab_size, 8, input_length=max_length)(input)
x = Flatten()(x)
x = Dense(1, activation='sigmoid')(x)
model = Model(inputs=input, outputs=x)
# compile the model
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
# summarize the model
print(model.summary())
# fit the model
model.fit(padded_docs, labels, epochs=50, verbose=0)
# evaluate the model
loss, accuracy = model.evaluate(padded_docs, labels, verbose=0)
print('Accuracy: %f' % (accuracy * 100))
После этого мы можем сохранить веса, полученные на слое внедрения, в файл для последующего использования в других моделях.
Часто эту модель также можно использовать для классификации других документов с тем же словарем, что и в тестовом наборе данных.
Далее давайте посмотрим на загрузку предварительно обученных вложений слов в Keras.
Пример использования предварительно обученных вложений GloVE
Слои внедрения Keras также могут использовать встроенные слова, изученные в другом месте.
В области обработки естественного языка принято изучать, сохранять и делиться предоставленными вложениями слов.
Например, исследователи метода GloVe предоставляют набор предварительно обученных вложений слов, выпущенных под лицензией общественного достояния. Видеть:
Самый маленький пакет весит 822 Мб и называется "glove.6B.zip". Он был обучен на наборе данных из 1 миллиарда словарных запасов (слов) с размером словаря 400 000 слов и несколькими различными размерами векторов встраивания, включая размер 50, 100, 200 и 300.
Вы можете загрузить эту коллекцию вложений, которые можно использовать в качестве весов для предварительно обученных вложений для слов в наборе обучающих данных в слое встраивания Keras.
Этот пример вдохновлен примером из проекта Keras:pretrained_word_embeddings.py.
После скачивания и разархивирования вы увидите несколько файлов, один из которых "glove.6B.100d.txt", который содержит 100-мерную версию вставки.
Если вы заглянете внутрь файла, вы увидите токен (слово), за которым следует вес (100 чисел) для каждой строки. Например, ниже приведена первая строка встроенного текстового файла ASCII, показывающая встраивание «the».
the -0.038194 -0.24487 0.72812 -0.39961 0.083172 0.043953 -0.39141 0.3344 -0.57545 0.087459 0.28787 -0.06731 0.30906 -0.26384 -0.13231 -0.20757 0.33395 -0.33848 -0.31743 -0.48336 0.1464 -0.37304 0.34577 0.052041 0.44946 -0.46971 0.02628 -0.54155 -0.15518 -0.14107 -0.039722 0.28277 0.14393 0.23464 -0.31021 0.086173 0.20397 0.52624 0.17164 -0.082378 -0.71787 -0.41531 0.20335 -0.12763 0.41367 0.55187 0.57908 -0.33477 -0.36559 -0.54857 -0.062892 0.26584 0.30205 0.99775 -0.80481 -3.0243 0.01254 -0.36942 2.2167 0.72201 -0.24978 0.92136 0.034514 0.46745 1.1079 -0.19358 -0.074575 0.23353 -0.052062 -0.22044 0.057162 -0.15806 -0.30798 -0.41625 0.37972 0.15006 -0.53212 -0.2055 -1.2526 0.071624 0.70565 0.49744 -0.42063 0.26148 -1.538 -0.30223 -0.073438 -0.28312 0.37104 -0.25217 0.016215 -0.017099 -0.38984 0.87424 -0.72569 -0.51058 -0.52028 -0.1459 0.8278 0.27062
Как описано в предыдущем разделе, первый шаг — определить эти примеры, закодировать их как целые числа, а затем дополнить эти последовательности до одинаковой длины.
В этом случае нам нужно иметь возможность отображать слова в целые числа и целые числа в слова.
Керас предоставляетTokenizerкласс, который может быть адаптирован к обучающим данным для последовательного преобразования текста в последовательности путем вызова метода texts_to_sequences() класса Tokenizer и имеет доступ к целочисленному словарному сопоставлению слов в атрибуте word_index.
# define documents
docs = ['Well done!',
'Good work',
'Great effort',
'nice work',
'Excellent!',
'Weak',
'Poor effort!',
'not good',
'poor work',
'Could have done better.']
# define class labels
labels = [1,1,1,1,1,0,0,0,0,0]
# prepare tokenizer
t = Tokenizer()
t.fit_on_texts(docs)
vocab_size = len(t.word_index) + 1
# integer encode the documents
encoded_docs = t.texts_to_sequences(docs)
print(encoded_docs)
# pad documents to a max length of 4 words
max_length = 4
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
print(padded_docs)
Затем нам нужно загрузить весь файл встраивания слов Glove в память как словарь слов для встраивания массива.
# load the whole embedding into memory
embeddings_index = dict()
f = open('glove.6B.100d.txt')
for line in f:
values = line.split()
word = values[0]
coefs = asarray(values[1:], dtype='float32')
embeddings_index[word] = coefs
f.close()
print('Loaded %s word vectors.' % len(embeddings_index))
Это медленно. Вложения, которые фильтруют специальные слова в обучающих данных, могут быть лучше.
Далее нам нужно создать матрицу встраивания для каждого слова в обучающем наборе данных. Мы можем сделать это, перечислив все уникальные слова в Tokenizer.word_index и найдя вектор веса встраивания из загруженных вложений GloVe.
Результатом является матрица весов только для слов, которые будут видны во время обучения.
# create a weight matrix for words in training docs
embedding_matrix = zeros((vocab_size, 100))
for word, i in t.word_index.items():
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
Теперь мы можем определить нашу модель, как раньше, и оценить ее.
Ключевое отличие состоит в том, что слой внедрения может быть заполнен весами внедрения слова GloVe. Мы выбрали 100-мерную версию, поэтому мы должны использовать output_dim, чтобы установить его равным 100, чтобы определить слой внедрения. Наконец, мы не хотим обновлять изученные веса слов в этой модели, поэтому мы установим для обучаемого свойства модели значение False.
e = Embedding(vocab_size, 100, weights=[embedding_matrix], input_length=4, trainable=False)
Полный рабочий пример приведен ниже.
from numpy import asarray
from numpy import zeros
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import Embedding
# define documents
docs = ['Well done!',
'Good work',
'Great effort',
'nice work',
'Excellent!',
'Weak',
'Poor effort!',
'not good',
'poor work',
'Could have done better.']
# define class labels
labels = [1,1,1,1,1,0,0,0,0,0]
# prepare tokenizer
t = Tokenizer()
t.fit_on_texts(docs)
vocab_size = len(t.word_index) + 1
# integer encode the documents
encoded_docs = t.texts_to_sequences(docs)
print(encoded_docs)
# pad documents to a max length of 4 words
max_length = 4
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
print(padded_docs)
# load the whole embedding into memory
embeddings_index = dict()
f = open('../glove_data/glove.6B/glove.6B.100d.txt')
for line in f:
values = line.split()
word = values[0]
coefs = asarray(values[1:], dtype='float32')
embeddings_index[word] = coefs
f.close()
print('Loaded %s word vectors.' % len(embeddings_index))
# create a weight matrix for words in training docs
embedding_matrix = zeros((vocab_size, 100))
for word, i in t.word_index.items():
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
# define model
model = Sequential()
e = Embedding(vocab_size, 100, weights=[embedding_matrix], input_length=4, trainable=False)
model.add(e)
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
# compile the model
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
# summarize the model
print(model.summary())
# fit the model
model.fit(padded_docs, labels, epochs=50, verbose=0)
# evaluate the model
loss, accuracy = model.evaluate(padded_docs, labels, verbose=0)
print('Accuracy: %f' % (accuracy*100))
Запуск примера может занять больше времени, но он показывает, что его можно адаптировать к этой простой задаче.
[[6, 2], [3, 1], [7, 4], [8, 1], [9], [10], [5, 4], [11, 3], [5, 1], [12, 13, 2, 14]]
[[ 6 2 0 0]
[ 3 1 0 0]
[ 7 4 0 0]
[ 8 1 0 0]
[ 9 0 0 0]
[10 0 0 0]
[ 5 4 0 0]
[11 3 0 0]
[ 5 1 0 0]
[12 13 2 14]]
Loaded 400000 word vectors.
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (None, 4, 100) 1500
_________________________________________________________________
flatten_1 (Flatten) (None, 400) 0
_________________________________________________________________
dense_1 (Dense) (None, 1) 401
=================================================================
Total params: 1,901
Trainable params: 401
Non-trainable params: 1,500
_________________________________________________________________
Accuracy: 100.000000
На практике лучше попытаться выучить встраивания слов с помощью предварительно обученных вложений, так как оно фиксировано, и попытаться выучить поверх предварительно обученных вложений, что похоже на использование предварительно обученного VGG в компьютерном зрении или наоборот. такие проблемы, связанные с чистой миграцией.
Однако это зависит от того, что лучше всего подходит для вашей конкретной проблемы.
Набор данных IMDB Экземпляр внедрения
from keras.models import Sequential,Model
from keras.layers import Flatten, Dense, Embedding, Input
input_layer = Input(shape=(maxlen,))
x = Embedding(input_dim=10000,output_dim=8)(input_layer)
# 单独做一个embedding模型,利于后面观察
embedding = Model(input_layer,x)
x = Flatten()(x)
x = Dense(1,activation='sigmoid')(x)
model = Model(input_layer,x)
model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])
model.summary()
history = modhistory = modhistory = mod> history = model.fit(x_train,y_train,epochs=10,batch_size=32,validation_split=0.2)
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_4 (InputLayer) (None, 20) 0
_________________________________________________________________
embedding_5 (Embedding) (None, 20, 8) 80000
_________________________________________________________________
flatten_5 (Flatten) (None, 160) 0
_________________________________________________________________
dense_5 (Dense) (None, 1) 161
=================================================================
Total params: 80,161
Trainable params: 80,161
Non-trainable params: 0
_________________________________________________________________
Train on 20000 samples, validate on 5000 samples
Epoch 1/10
20000/20000 [==============================] - 2s 105us/step - loss: 0.6772 - acc: 0.6006 - val_loss: 0.6448 - val_acc: 0.6704
Epoch 2/10
20000/20000 [==============================] - 2s 93us/step - loss: 0.5830 - acc: 0.7188 - val_loss: 0.5629 - val_acc: 0.7046
Epoch 3/10
20000/20000 [==============================] - 2s 95us/step - loss: 0.5152 - acc: 0.7464 - val_loss: 0.5362 - val_acc: 0.7208
Epoch 4/10
20000/20000 [==============================] - 2s 93us/step - loss: 0.4879 - acc: 0.7607 - val_loss: 0.5299 - val_acc: 0.7292
Epoch 5/10
20000/20000 [==============================] - 2s 97us/step - loss: 0.4731 - acc: 0.7694 - val_loss: 0.5290 - val_acc: 0.7334
Epoch 6/10
20000/20000 [==============================] - 2s 98us/step - loss: 0.4633 - acc: 0.7773 - val_loss: 0.5317 - val_acc: 0.7344
Epoch 7/10
20000/20000 [==============================] - 2s 96us/step - loss: 0.4548 - acc: 0.7819 - val_loss: 0.5333 - val_acc: 0.7318
Epoch 8/10
20000/20000 [==============================] - 2s 93us/step - loss: 0.4471 - acc: 0.7870 - val_loss: 0.5377 - val_acc: 0.7288
Epoch 9/10
20000/20000 [==============================] - 2s 95us/step - loss: 0.4399 - acc: 0.7924 - val_loss: 0.5422 - val_acc: 0.7278
Epoch 10/10
20000/20000 [==============================] - 2s 90us/step - loss: 0.4328 - acc: 0.7957 - val_loss: 0.5458 - val_acc: 0.7290
Давайте посмотрим на форму ввода
x_train[1].shape
x_train[1]
x_train[:1].shape
x_train[:1]
(20,)
array([ 23, 4, 2, 15, 16, 4, 2, 5, 28, 6, 52, 154, 462,
33, 89, 78, 285, 16, 145, 95], dtype=int32)
(1, 20)
array([[ 65, 16, 38, 2, 88, 12, 16, 283, 5, 16, 2, 113, 103,
32, 15, 16, 2, 19, 178, 32]], dtype=int32)
Посмотрите на результат встраивания еще раз,
embedding.predict(x_train[:1]).shape
embedding.predict(x_train[:1])
(1, 20, 8)
array([[[-0.17401133, -0.08743777, 0.15631911, -0.06831486, -0.09105065,
0.06253908, -0.0798945 , 0.07671431],
[ 0.18718374, 0.10347525, -0.06668846, 0.25818944, 0.07522523,
0.07082067, 0.05170904, 0.22902426],
[ 0.06872956, -0.00586612, 0.07713806, -0.00182899, 0.00882899,
-0.18892162, -0.13580748, -0.03166043],
[-0.01912907, -0.01732869, 0.00391375, -0.02338142, 0.02787969,
-0.02744135, 0.0074541 , 0.01806928],
[ 0.20604047, 0.10910885, 0.06304865, -0.14038748, 0.12123005,
0.06124007, 0.0532628 , 0.17591232],
[-0.19636872, -0.0027669 , 0.01087157, -0.02332311, -0.04321857,
-0.09228673, -0.03061322, -0.13376454],
[ 0.18718374, 0.10347525, -0.06668846, 0.25818944, 0.07522523,
0.07082067, 0.05170904, 0.22902426],
[-0.27160701, -0.29296583, 0.1055108 , 0.15896739, -0.24833643,
-0.17791845, -0.27316946, -0.241273 ],
[-0.02175452, -0.0839383 , 0.04338101, 0.01062139, -0.11473208,
-0.18394938, -0.05141308, -0.10405254],
[ 0.18718374, 0.10347525, -0.06668846, 0.25818944, 0.07522523,
0.07082067, 0.05170904, 0.22902426],
[-0.01912907, -0.01732869, 0.00391375, -0.02338142, 0.02787969,
-0.02744135, 0.0074541 , 0.01806928],
[-0.14751843, 0.05572686, 0.20332271, -0.01759946, -0.0946402 ,
-0.14416233, 0.16961734, 0.01381243],
[ 0.00282665, -0.17532936, -0.09342033, 0.04514923, -0.04684081,
0.1748796 , -0.09669576, -0.10699435],
[ 0.00225757, -0.12751001, -0.12703758, 0.17167819, -0.03712473,
0.04252302, 0.04741228, -0.02731293],
[ 0.02198115, 0.03989581, 0.13165356, 0.06523556, 0.14900513,
0.01858517, -0.01644249, -0.02377043],
[ 0.18718374, 0.10347525, -0.06668846, 0.25818944, 0.07522523,
0.07082067, 0.05170904, 0.22902426],
[-0.01912907, -0.01732869, 0.00391375, -0.02338142, 0.02787969,
-0.02744135, 0.0074541 , 0.01806928],
[-0.01993229, -0.04436176, 0.07624088, 0.04268746, -0.00883252,
0.00789542, -0.03039453, 0.05851226],
[-0.12873659, -0.00083202, -0.03246918, 0.23910245, -0.24635716,
0.10966355, 0.02079294, -0.03829115],
[ 0.00225757, -0.12751001, -0.12703758, 0.17167819, -0.03712473,
0.04252302, 0.04741228, -0.02731293]]], dtype=float32)
Можно видеть, что слой внедрения встраивает входную выборку (1, 20) (предложение до 20 слов, где каждое слово представлено как целочисленное число) в виде вектора (1, 20, 8), то есть , каждое слово встраивается в виде 8-мерного вектора, а параметры всего слоя встраивания изучаются нейронной сетью.После того, как данные проходят через слой встраивания, они легко преобразуются в формат, пригодный для дальнейшей обработки CNN или РНН.