Pull to refresh

Эксперемент с классификацией текста с использованием Tensor Flow

Весьма успешно поэкспериментировал с Tensor Flow в плане классификации текста и решил написать короткую статью об этом. Мои знания в области нейросетей не так уж велики, но после того, как прочитал статью, я понял, что есть тенденция чрезмерно усложнять подход к обработке текста и, возможно, кому-то будет полезно увидеть гораздо более простой и вполне рабочий подход к классификации коротких сообщений.


Я придумал себе следующую задачу: используя базу данных AMAZON_FASHION_5.json (скачать можно тут), научиться по написанному комментарию пользователя к товару предсказывать оценку пользователя. Запись базы данных имеет следующий вид:
{"overall": 5.0, "verified": true, "reviewTime": "06 28, 2018", "reviewerID": "A2IBS6PIPAGAB5", "asin": "B0014F7B98", "style": {"Size:": " 5 B(M) US", "Color:": " Wolf Grey/Black-pink Blast/White"}, "reviewerName": "J. Avila", "reviewText": "My favorite cross trainers!", "summary": "Comfortable", "unixReviewTime": 1530144000}


Я использовал только 2 поля из каждой записи: overall — оценка и reviewText — текстовый отзыв.
Несмотря на обилие в интернете описаний сложных методик для обработки текста с построением словарей и т.д., я решил сделать все проще — подать сообщение на вход нейросети просто как коды символов. Полный диапозон ASCII передавать бессмысленно, так как слишком большое количество неинформативных кодов затруднит обучение. Поэтому я использовал свою таблицу кодов: коды 0-25 соответствуют 'A'-'Z', коды 30-39 — '0'-'9'. Коды 26-29 не использовались. Остальные символы я отбросил (хотя восклицательный знак, к примеру, тоже мог пригодиться — но и без него все работает весьма неплохо). На вход нейросети поступает массив из 500 float значений, каждое из которых рассчитывается как symbol_code/40.0. На выходе 5 нейронов, каждый из которых содержит вероятность конкретной оценки: первый нейрон содержит вероятность оценки 1, второй — 2 и т.д. Можно было просто получать на выходе оценку на одном нейроне, но хотелось немного усложнить и решить именно задачу классификации.


Далее привожу полный текст программы (я это запилил сегодня в течение часа, так что прошу быть снисходительными к качеству решения:) ).


import tensorflow as tf
import numpy as np
import json

inputSize = 500
symbolsCount = 40
noteMax = 5

testsCount = 100

file = open("D:/Temp/textdataset.json")
lines = file.readlines()

#To prepare the data for NN training.
inputData = np.zeros([len(lines), inputSize], np.float32)
results = np.zeros([len(lines), noteMax], np.float32)

#Currently processed line of the file
lineId = 0

for line in lines:
    data = json.loads(line)
    if "reviewText" not in data:
        continue

    text = data["reviewText"]
    text = text.lower()

    result = int(data["overall"])

    #Convert the line of ASCII to follwoing encoding: 0-25 - 'A'-'Z', 30-39 - '0' - '9'
    for i in range(min(len(text), symbolsCount)):
        symb = ord(text[i])
        symbId = 0

        if ord('a') <= symb <= ord('z'):
            symbId = symb - ord('a')
        elif ord('0') <= symb <= ord('9'):
            symbId = symb - ord('0') + 30

        inputData[lineId, i] = float(symbId)/symbolsCount

    #Set 1 for for the concrete note which is selected by the reporter. Other outputs are 0
    results[lineId, result - 1] = 1.0
    lineId += 1

#Remove extra items in the data. We have this data because part of lines had no reviewText attribute.
results = results[:lineId]
inputData = inputData[:lineId]

#Model of 5 layers with LeakyReLU activation for first 4 and Softmax for the last one.
model = tf.keras.models.Sequential([
    tf.keras.layers.Dense(1024, input_shape=[inputSize]),
    tf.keras.layers.LeakyReLU(0.2),
    tf.keras.layers.Dense(1024),
    tf.keras.layers.LeakyReLU(0.2),
    tf.keras.layers.Dense(512),
    tf.keras.layers.LeakyReLU(0.2),
    tf.keras.layers.Dense(256),
    tf.keras.layers.LeakyReLU(0.2),
    tf.keras.layers.Dense(5, activation=tf.keras.activations.softmax)
])

#Optimizer - Adam. It is used with low learning rate and small beta_1 which means actually that RMSprop can be used instead:) - but it was not tested.
optimizer = tf.keras.optimizers.Adam(learning_rate=0.0001, beta_1=0.5)
model.compile(optimizer=optimizer, loss=tf.keras.losses.BinaryCrossentropy())

#Training data
x = inputData[:-testsCount]
y = results[:-testsCount]
#Training of the model
model.fit(x, y, batch_size=300, epochs=100)

#Testing
expected = results[-testsCount:]
predicted = model.predict(inputData[-testsCount:])

res = np.concatenate([predicted, expected], axis=1).reshape([testsCount, 2, 5])
print(res)

errorsCount = 0

#Check test results
for i in range(testsCount):
    for j in range(noteMax):
        if expected[i, j] == 1.0 and predicted[i, j] <= 0.5:
            errorsCount += 1

print("Errors count is ", errorsCount, " from ", testsCount)

Обучается сеть очень быстро и дает неплохую точность: 2% ошибок на тестовых данных — они не были включены в обучающий dataset. Надеюсь кому-то пригодится.

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.