Рассмотрим как в простейшем случае решается задача определения автора текста.
- Дано: тексты 6+1 авторов в *.txt, кодировка UTF8. Разделены на обучающую и проверочную выборку, т.е. на каждого автора по 2 текста. +1 — два текста Марининой идут в отдельном архиве.
- Нужно: решить задачу классификации, т.е. по тексту определить автора.
Анализ текста буду проводить с помощью простейшей сети в Colab с помощью библиотеки Keras. Решение задачи доcтаточно подробно разобрано основателем «Университета искусственного интеллекта» Дмитрием Романовым. Однако, мне для лучшего усвоения материала предпочтительнее текстовый вариант с максимально детальным разбором ключевых моментов. Поэтому написал вот этот расширенный конспект лекции, чтобы глубже разобраться в теме. Colab notepad с моими экспериментами здесь.
Импорт библиотек
from google.colab import files import numpy as np import pandas as pd import os import matplotlib.pyplot as plt %matplotlib inline from tensorflow.keras import utils from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout, SpatialDropout1D, BatchNormalization, Embedding, Flatten, Activation, SimpleRNN from tensorflow.python.keras.optimizers import Adam, RMSprop from keras.preprocessing.text import Tokenizer, text_to_word_sequence from sklearn.model_selection import train_test_split
Загрузка файлов
Тексты авторов достаточно объемные и для удобства работы с Colab размещены в сжатом zip файле, чтобы удобно и быстро скачивать при дисконнектах notebook. Естественно, можно использовать загрузку файлов вручную, но это все отнимает время. Кроме того при перезапусках notebook, если правильно написать подгрузку данных, чтобы код не задавал лишних вопросов, можно запускать на выполнение с помощью команды «Run before». Это значительно уменьшает трудозатраты при повторных перезапусках.
Для удобства я использую следующий подход. Функция, которая подгружает файл в Colab с ftp/http с помощью WGET:
#Загружаем архив с текстами с FTP/HTTP def loadfiles(filename, fullpath): loaded = True if os.path.isfile(filename): print("File \"", filename, "\" has already downloaded.") else: print("Start downloading", filename) !wget $fullpath !unzip $filename loaded = False return loaded !ls
Для вызова функции используем следующий код:
URL = "http://www.bizkit.ru/wp-content/uploads/COLAB/" filename = "Writers_texts.zip" fullpath = URL + filename print(fullpath) loaded = loadfiles(filename, fullpath)
Иногда нужно удалить старые файлы при загрузке обновленного контента. По-умолчанию код закомментарен.
if loaded: #!rm $filename
Поскольку тексты авторов находятся в zip архиве — нужно их распаковать, поэтому в функции вызывается код:
!unzip $filename
Естественно, можно распаковывать тексты в определенную папку на Colab, но при дисконнекте notebook данные затираются, поэтому в этом нет особого смысла.
Загрузка данных
При загрузке данных может потребоваться их вычистка для удаления незначимых слов или символов. Можно использовать штатный replace() или сделать множественную замену, передав в качестве аргумента список заменяемых слов. Функция очень простая:
def replaceMultiple(mainString, toBeReplaces, newString): # Iterate over the strings to be replaced for elem in toBeReplaces: # Check if string is in the main string if elem in mainString: # Replace the string mainString = mainString.replace(elem, newString) return mainString
Пример использования:
txt = replaceMultiple("Тестовая - запись — верна", ['\n\r', '\n', '\r', '-', '—'] , " ") print(txt) Тестовая запись верна
Исходный текст загружаем в строковую переменную для дальнейшей обработки:
def readText(fileName): #функция принимает имя файла f = open(fileName, 'r') #открываем файла в режиме чтения text = f.read() #читаем текст #text = replaceMultiple(text, ['\n\r', '\n', '\r', '\\xa0'] , " ") #заменяем переносы и спецсимволы разделителей на пробелы return text #функция возвращает текст файла
И подгружаем в список (list) тексты всех анализируемых авторов:
#Загружаем обучающие тексты trainText = [] trainText.append(readText('(О. Генри) Обучающая_50 вместе.txt')) trainText.append(readText('(Стругацкие) Обучающая_5 вместе.txt')) trainText.append(readText('(Булгаков) Обучающая_5 вместе.txt')) trainText.append(readText('(Клиффорд_Саймак) Обучающая_5 вместе.txt')) trainText.append(readText('(Макс Фрай) Обучающая_5 вместе.txt')) trainText.append(readText('(Рэй Брэдберри) Обучающая_22 вместе.txt')) trainText.append(readText('Маринина - Бой тигров в долине. Том 1 .txt')) className = ["О. Генри", "Стругацкие", "Булгаков", "Саймак", "Фрай", "Брэдбери", "Маринина"] nClasses = len(className) #Загружаем тестовые тексты testText = [] testText.append(readText('(О. Генри) Тестовая_20 вместе.txt')) testText.append(readText('(Стругацкие) Тестовая_2 вместе.txt')) testText.append(readText('(Булгаков) Тестовая_2 вместе.txt')) testText.append(readText('(Клиффорд_Саймак) Тестовая_2 вместе.txt')) testText.append(readText('(Макс Фрай) Тестовая_2 вместе.txt')) testText.append(readText('(Рэй Брэдберри) Тестовая_8 вместе.txt')) testText.append(readText('Маринина - Бой тигров в долине. Том 2 .txt')) #Смотрим размеры загруженных выборок for i in range(len(trainText)): print("Длина обучающего текста", className[i], "\t", len(trainText[i]), "\tПроверочного:", "\t", len(testText[i]))
Для загрузки текстов можно использовать более компактный код используя ‘(‘ в качестве токена для определения нужных файлов:
writers_text=[] for files in os.listdir(): if (files.startswith('(')): with open(files, 'r') as f: text = f.read() print("Файл:", files, 'длина:', len(text)) writers_text.append(text)
В двух списках содержатся тексты каждого из 6+1 авторов для обучающей и проверочной выборки:
Длина обучающего текста О. Генри 1049517 Проверочного: 349662 Длина обучающего текста Стругацкие 2042469 Проверочного: 704846 Длина обучающего текста Булгаков 1765648 Проверочного: 875042 Длина обучающего текста Саймак 1609507 Проверочного: 318811 Длина обучающего текста Фрай 3700010 Проверочного: 1278191 Длина обучающего текста Брэдбери 1386454 Проверочного: 868673 Длина обучающего текста Маринина 504955 Проверочного: 474573
В случае c Марининой я взял первый том книги «Бой тигров в долине» в качестве обучающей выборки, а второй — в качестве проверочной. Кстати, включение книг Марининой в список слегка ухудшило качество распознавания с 99% до 97% Впрочем, однозначно утверждать нельзя, поскольку исходные веса нейронной сети выбираются случайным образом и локальный оптимум мог быть найден не лучший.
Предобработка данных
Собственно, предобработка данных — один из ключевых моментов получения хороших результатов обучения сети. Самый первый шаг — разбить исходный текст на слова, убрав незначимые символы: знаки пунктуации, спецсимволы, переносы и пр.
Для такой предобработки в Keras есть класс Tokenizer. Его методы довольно подробно рассмотрены здесь. Немного поясню, что он делает.
При инициализации класса ему в качестве аргумента передается набор параметров:
- num_words — количество слов, которое вернет Tokenizer. Рассчитывается статистика повторяемости всех слов после фильтрации. Затем сортируются в порядке убываемости и берутся первые num_words слов.
- filters — какие символы исключить из текста. Стандартный набор символов: ‘!–»—#$%&()*+,-./:;<=>?@[\]^_`{|}~\t\n\r’
- lower = True — приводит к нижнему регистру, чтобы исключить разницу между одинаковыми словами в разных регистрах.
- split=’ ‘ — разделение слов по символу пробела.
- char_level=False — если False, то текст делится на слова, а если True — на символы.
- oov_token — если указан токен, то на него будут заменятся не попавшие в словарь слова при вызове метода text_to_sequence.
maxWordsCount = 20000 #макс. кол-во слов/индексов для обучения текстов tokenizer = Tokenizer(num_words=maxWordsCount, filters='!–"—#$%&amp;()*+,-./:;&lt;=>?@[\\]^_`{|}~\t\n\r«»', lower=True, split=' ', char_level=False) tokenizer.fit_on_texts(trainText) #передаем тексты для получения токенов отсортированных по частоте повторяемости в количестве maxWordsCount items = list(tokenizer.word_index.items()) #берем индексы слов для просмотра print(items[:20])
Если посмотреть на результат, то на первых позициях будут стоять наиболее часто используемые слова в текстах всех авторов.
[('и', 1), ('в', 2), ('не', 3), ('я', 4), ('что', 5), ('на', 6), ('с', 7), ('он', 8), ('а', 9), ('как', 10), ('то', 11), ('это', 12), ('но', 13), ('все', 14), ('у', 15), ('по', 16), ('его', 17), ('к', 18), ('так', 19), ('мне', 20)]
Стоит или не стоит оставлять союзы при распознавании текста авторов — вопрос. Это гипотеза для проверки. Чтобы убрать — достаточно добавить соответствующие односимвольные разделители в filters.
Какие ещё полезные свойства есть у Tokenizer?
print(tokenizer.word_index['лиса']) #позиция/индекс слова в массиве токенов print(tokenizer.word_docs['лиса']) #в скольки источниках встретилось слово print(tokenizer.word_counts['лиса']) #количество повторений слов 4918 6 35
Соотвественно, в отсортированном списке слов-токенов ‘лиса’ находится на 4918 позиции. Встретилась в 6 книгах из 7 и использовалось 35 раз.
Если вывести список слов со статистикой повторений, то видно, что слова располагаются в той-же последовательности, как в исходном тексте.
dist = list(tokenizer.word_counts.items()) print(dist[:20]) print(trainText[0][:100]) [('лиса', 35), ('на', 27709), ('рассвете', 65), ('коралио', 91), ('нежился', 6), ('в', 45508), ('полуденном', 2), ('зное', 3), ('как', 13748), ('томная', 2)] «Лиса-на-рассвете» Коралио нежился в полуденном зное, как томная красавица в сурово хранимом гарем
Имея список с количеством повторений можно убрать слова, которые встречаются в текстах меньше определенного количества раз. Это может быть полезно для очистки текстов от редко повторяющися уникальных слов, которые встречаются только у одного автора. Такие слова могут вводить в заблуждение сеть, поскольку она будет с высокой вероятностью сопоставлять такое слово с автором. Как только появится автор, испольующий такие-же редкие слова, может потребоватся переобучение сети, чтобы она не путала авторов.
count_thres = 2 #кол-во раз меньше которого слово нужно исключить из списка low_count_words = [w for w,c in tokenizer.word_counts.items() if c < count_thres] #создаем список слов встречаюихся менее count_thres print(len(low_count_words)) print(low_count_words[:20]) for w in low_count_words: #удаляем такие слова из исходного списка del tokenizer.word_index[w] del tokenizer.word_docs[w] del tokenizer.word_counts[w] 63955 ['хранимом', 'брильянтиком', 'вкрапленным', 'нависая', 'тюремщик', 'сейбах', 'склоняли', 'кордебалет', 'прима', 'busca']
Заметим, что найдено 63955 слов встречающиеся в текстах менее 1 раза (только у одного автора). Необходимо проверять гипотезу, возрастет ли качество распознавания если исключить такие «шумовые» слова.
После предобработки нужно преобразовать текст в последовательность индексов в соответствии с частотным словарем.
trainWordIndexes = tokenizer.texts_to_sequences(trainText) #обучающие тесты в индексы testWordIndexes = tokenizer.texts_to_sequences(testText) #проверочные тесты в индексы print("Исходный текст:\t\t", trainText[1] [:87]) print("Он же в виде последовательности индексов:\t", trainWordIndexes[1][:20]) Исходный текст: Парень из преисподней 1 Ну и деревня! Сроду я таких деревень не видел и не знал Он же в виде последовательности индексов: [464, 21, 1537, 46, 1, 12384, 7750, 4, 399, 3, 254, 1, 3, 247, 54, 5, 225, 9305, 2577, 180]
Статистика по обучающим текстам:
symbs = 0; words = 0; for i in range(len(trainText)): print(className[i], "\t", len(trainText[i]), "символов,", len(trainWordIndexes[i]), " слов") symbs += len(trainText[i]) words += len(trainWordIndexes[i]) print("\r\nВ сумме:\t", symbs, "символов,", words, "слов") print() print("Статистика по обучающим текстам:") symbs = 0; words = 0; for i in range(len(testText)): print(className[i], "\t", len(testText[i]), " символов, ", len(testWordIndexes[i]), " слов") symbs += len(testText[i]) words += len(testWordIndexes[i]) print("\r\nВ сумме:\t", symbs, "символов,", words, "слов") О. Генри 1049517 символов, 131923 слов Стругацкие 2042469 символов, 264513 слов Булгаков 1765648 символов, 214398 слов Саймак 1609507 символов, 221265 слов Фрай 3700010 символов, 501686 слов Брэдбери 1386454 символов, 183143 слов Маринина 504955 символов, 68633 слов В сумме: 12058560 символов, 1585561 слов Статистика по обучающим текстам: О. Генри 349662 символов, 41947 слов Стругацкие 704846 символов, 87409 слов Булгаков 875042 символов, 106240 слов Саймак 318811 символов, 42715 слов Фрай 1278191 символов, 164646 слов Брэдбери 868673 символов, 106731 слов Маринина 474573 символов, 61716 слов В сумме: 4869798 символов, 611404 слов
Создание обучающей и проверочной выборки
На предыдущем шаге в trainWordIndexes получили последовательность индексов представляющих слово в исходном тексте. Чем чаще слово появляется в тексте, тем меньше индекс.
Если использовать one hot encoding (OHE) над каждым списком, например, 0-й книги, то получим len(trainWordIndexes[0]) векторов длиной по maxWordsCount = 20000 (общее количество слов в словаре). Каждый вектор содержит лишь одну 1-цу на позиции соотвествующей индексу. Разреженная матрица (sparse matrix) в которой преимущественно нули.
Дальше формируется Bag of Words (BOW). Фактически из разреженной матрицы создается вектор длиной maxWordsCount = 20000. В нем 1-цы стоят на позициях, где слово есть и 0, где нет. Грубо говоря для текста в 20000 слов, все слова которого использовались для создания словаря на всех позициях будут стоять 1-цы, нолей не будет.
Например, после сортировки слов в тексте было получено, что слово кошка встречается довольно часто и индекс равен 8.
Кошка [0 0 0 0 0 0 0 0 1 0 0 0 0 ... 0 0 0] OR Ушла [0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 1 0] OR Спать [0 0 0 0 0 0 0 1 0 0 0 0 0 ... 0 0 0] ______________________________________________ BOW: [0 0 0 0 0 0 0 1 1 0 0 0 0 ... 0 1 0]
На вход сети нужно подать one hot encoding (OHE). Можно обрезать исходный текст до необходимой длины (pad sequence) и сделать OHE. Если сделать OHE над входной последовательностью индексов после обрезания, то нули будут в тех позициях в векторе, где слово было отброшено.
Очевидно, что такой подход убирает информацию о взаимном расположении слов в тексте. Это один из минусов подхода, поскольку контекст полностью убирается. В ряде случаев такое упрощение может ухудшать понимание сетью смысла текста.
Другой минус подхода — при больших словарях длина вектора будет значительной и на операции с ними будет расходоваться много памяти и вычислительных ресурсов.
Для добавления информации о взаимных расположениях слова в тексте используют n-граммы (n-grams). Если используется комбинация двух слов, то получаем биграмму (2-грамму), трех — триграмму. В данном примере такое усложнение не требуется. Даже при таком упрощении нейронная сеть обеспечивает хорошее качество распознавания текстов авторов.
Для увеличения размера обучающей последовательности для текстов используют следующий трюк. Берут «виртуальное окно» длиной xLen и продергивают его вдоль последовательности индексов слов входного текста со смещением shift. Всё, что попало в «окно» добавляют в элемент массива в качестве нового блока текста. Таким образом размер обучающей выборки многократно возрастает, при этом, в данном случае, с текстом не происходит критичных статистических изменений, которые могли бы сказаться на качестве обучения.
########################### # Формирование обучающей выборки по листу индексов слов # (разделение на короткие векторы) ########################## def getSetFromIndexes(wordIndexes, xLen, shift): xSample = [] wordsLen = len(wordIndexes) index = 0 #Идём по всей длине вектора индексов #"Выбираем" блоки текст длины xLen и смещаемся вперёд на shift while (index + xLen <= wordsLen): xSample.append(wordIndexes[index:index+xLen]) index += shift return xSample #Тестируем работу функции arr = [x for x in range(23)] print("Длина:", len(arr), "\r\n") print("Входной текст:", arr, "\r\n") indexes = getSetFromIndexes(arr, 10, 3) for i in range(len(indexes)): print(indexes[i]) print("Общий размер:", len(indexes), "x", len(indexes[0]), '=', len(indexes[0]) * len(indexes)) Длина: 23 Входной текст: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [3, 4, 5, 6, 7, 8, 9, 10, 11, 12] [6, 7, 8, 9, 10, 11, 12, 13, 14, 15] [9, 10, 11, 12, 13, 14, 15, 16, 17, 18] [12, 13, 14, 15, 16, 17, 18, 19, 20, 21] Общий размер: 5 x 10 = 50
Для преобразования одномерного массива индексов в последовательность 0 и 1 для подачи на вход нейронной сети используется функция Keras: to_categorical():
arr = to_categorical(trainWordIndexes[0]) print(len(trainWordIndexes[0])) print(arr.shape) print(trainWordIndexes[0][:10]) print(arr[:20]) 131923 (131923, 20000) [4918, 6, 2599, 1820, 2, 10, 6230, 2, 3328, 274] [[0. 0. 0. ... 0. 0. 0.] [0. 0. 0. ... 0. 0. 0.] [0. 0. 0. ... 0. 0. 0.] ... [0. 0. 0. ... 0. 0. 0.] [0. 0. 1. ... 0. 0. 0.] [0. 0. 0. ... 0. 0. 0.]]
Объединим все тексты книг в одну обучающую матрицу и создадим для неё проверочную матрицу.
- В цикле проходимся по индексам слов текста каждой книги (для каждой книги одна строка матрицы). Длина каждой строки матрицы равна количеству слов в тексте книги.
- Конвертируем последовательность индексов слов с помощью «виртуального окна» (функция getSetFromIndexes) в расширенную обучающую матрицу.
- Для каждой строки обучающей матрицы создаем матрицу правильных ответов ySamples.
def createSetsMultiClasses(wordIndexes, xLen, step): #Для каждого из классов создаём обучающую/проверочную выборку из индексов nClasses = len(wordIndexes) #задаем количество классов выборки xSamples = [] #здесь будет список размером "суммарное кол-во окон во всех текстах*длину окна(например 15779*1000)" ySamples = [] #здесь будет список размером "суммарное кол-во окон во всех текстах*вектор длиной по количеству классов" for t, wI in enumerate(wordIndexes): tmp = getSetFromIndexes(wI, xLen, step) #получаем список индексов, разбитый на "кол-во окон * длину окна" xSamples += tmp ySamples += [utils.to_categorical(t, nClasses).tolist()] * len(tmp) return (np.array(xSamples), np.array(ySamples))
Проверяем как отрабатывает функция на тестовых данных:
texts = [] texts.append([x for x in range(23)]) #Тестовый текст 1 texts.append([x for x in range(100, 123)]) #Тестовый текст 2 xTrain, yTrain = createSetsMultiClasses(texts, 10, 3) print(xTrain) print(yTrain) [[ 0 1 2 3 4 5 6 7 8 9] [ 3 4 5 6 7 8 9 10 11 12] [ 6 7 8 9 10 11 12 13 14 15] [ 9 10 11 12 13 14 15 16 17 18] [ 12 13 14 15 16 17 18 19 20 21] [100 101 102 103 104 105 106 107 108 109] [103 104 105 106 107 108 109 110 111 112] [106 107 108 109 110 111 112 113 114 115] [109 110 111 112 113 114 115 116 117 118] [112 113 114 115 116 117 118 119 120 121]] [[1. 0.] [1. 0.] [1. 0.] [1. 0.] [1. 0.] [0. 1.] [0. 1.] [0. 1.] [0. 1.] [0. 1.]]
Как говорилось ранее, каждой строке обучающей матрицы ставится в соответствие номер строки в исходном массиве текстов (texts).
Подготовка данных
#Задаём базовые параметры xLen = 7000 #Длина отрезка текста в результирующемвекторе в словах shift= 100 #Смещение окна для разбиения исходного текста на обучающие вектора xTrain, yTrain = createSetsMultiClasses(trainWordIndexes, xLen, shift) #Формируем обучающую выборку xTest, yTest = createSetsMultiClasses(testWordIndexes, xLen, shift) #Формируем тестовую выборку print(xTrain.shape) print(yTrain.shape) print(xTest.shape) print(yTest.shape) (15369, 7000) (15369, 7) (5629, 7000) (5629, 7)
Конвертируем индексы слов в обучающей матрице в бинарную последовательность Bag of Words.
xTrain01 = tokenizer.sequences_to_matrix(xTrain.tolist()) #Конвертируем xTrain в список перед передачей методу xTest01 = tokenizer.sequences_to_matrix(xTest.tolist()) #Конвертируем xTest в список перед передачей методу print(xTrain01.shape) #Размер обучающей выборки, сформированной по Bag of Words print(xTrain01[0][100:120]) #фрагмент набора слов в виде Bag of Words (15369, 20000) [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 1. 1. 1. 0. 1. 1. 0. 1. 1.]
Нейросеть
При обучении полносвязной нейронной сети в качестве активационной функции чаще всего используют «softmax», однако в данном случае использование «sigmoid» позволяет существенно увеличить долю верных ответов на проверочной выборке до 97-99%.
Смысла увеличивать количество слоев или нейронов особо нет, велик риск получить переобучение.
Слой dropout(0.3) снижает вероятность переобучения. При меньших значениях нейронная сеть склонна к переобучению.
Важный момент — активационная функция. Если использовать часто применяемый softmax, то результаты существенно хуже, а если tanh, иногда рассматриваемый как более удачную альтернативу sigmoid, то ошибка возрастает колоссально.
#Создаём полносвязную сеть (FeedForward Neural Network) model01 = Sequential() model01.add(BatchNormalization()) model01.add(Dense(30, input_dim=maxWordsCount, activation="relu")) model01.add(Dropout(0.3)) model01.add(BatchNormalization()) model01.add(Dense(len(trainText), activation='sigmoid')) model01.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) #Обучаем сеть на выборке, сформированной по bag of words - xTrain01 history = model01.fit(xTrain01, yTrain, epochs=20, batch_size=128, validation_data=(xTest01, yTest)) plt.plot(history.history['acc'], label='Доля верных ответов на обучающем наборе') plt.plot(history.history['val_acc'], label='Доля верных ответов на проверочном наборе') plt.xlabel('Эпоха обучения') plt.ylabel('Доля верных ответов') plt.legend() plt.show()
На 9-й эпохе при dropout(0.4) получена точность распознавания 0,9922.
Распознавание
Если нейронная сеть смогла обобщить исходные данные, выделив характерные черты (features) авторов, то созданную модель можно использовать для распознавания текстов автора, на которых сеть не обучалась. Например, в случае Марининой первый том книи использовался для тренировки сети, а второй — для проверки, что модель полученная сетью позволяет ей с некоторой вероятностью идентифицировать автора текста.
Сначала подготовим исходный текст для подачи на полносвязную нейронную сеть. Функция prepareTextForRecognition преобразует текст в последовательность пригодную для распознавания.
def prepareTextForRecognition(testText, xLen, shift): #функция принимает последовательность индексов, размер окна, смещение окна testWordIndexes = tokenizer.texts_to_sequences([testText]) print("Source text:",testText[:100]) print("Indexes:",testWordIndexes[0][:10]) print(len(testWordIndexes[0])) sample = getSetFromIndexes(testWordIndexes[0], xLen, shift) print("Number of text blocks of xLen:", len(sample)) xTest = tokenizer.sequences_to_matrix(sample) return np.array(xTest)
srcTextBOW = prepareTextForRecognition(testText[3], xLen, step) #Получаем результаты распознавания класса по блокам слов длины xLen currPred = model01.predict(srcTextBOW) print(currPred.shape) print(currPred[:2]) #Определяем номер распознанного класса для каждого блока слов длины xLen currOut = np.argmax(currPred, axis=1) print(currOut)
Результат исполнения кода требует пояснения.
Source text: Зачарованное паломничество 1 Гоблин со стропил следил за прячущимся монахом, который шпионил за Indexes: [1537, 76, 3406, 22, 133, 22, 14059, 18330, 1, 977] 42715 Number of text blocks of xLen: 358 (358, 7) [[1.6987324e-05 1.8984079e-05 6.2286854e-06 3.5908818e-04 2.3365021e-05 2.4348497e-05 2.4139881e-06] [1.9401312e-05 2.1964312e-05 5.7220459e-06 3.5190582e-04 2.7537346e-05 2.3245811e-05 2.3245811e-06]] [3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 1 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 5 5 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3]
После подачи предобработанной последовательности на нейронную сеть на выходе получается матрица 358 х 7, где 358 — это количество блоков текстов длиной xLen, а 7 — количество классов для определения.
Нейронка производит классификацию по каждому из 358 блоков, голосуя вероятностями. Если один текст является плагиатом, то вероятности будут достаточно близкими. Также если нейронке для проверки «скормить» текст, на котором она не обучалась, то вероятности предсказания по идее должны быть близки без явного лидера. Хотя эту гипотезу надо проверять. Но это тема другого исследования.
Метод argmax() с указанием axis=1 говорит, что искать максимальное значение нужно по строкам, т.е. в каждой строке будет найден максимум и вернется его индекс в строке. Пример использования argmax:
arr = np.array([[0.1, 0.3, 0.2, 0.6, 0.9], [0.7, 0.3, 0.2, 0.6, 0.5], [0.8, 0.3, 0.9, 0.6, 0.3]]) print(np.argmax(arr, axis=1)) [4 0 2]
Визуально заметно, что по большинству блоков строк нейронная сеть отнесла их к классу 3. Если посчитать вероятности, то:
probability = [0]*nClasses for i in range(len(testText)): #Проходим по всем 7 классам и считаем вероятность отнесения текста к каждому из классов probability[i] = np.count_nonzero(currOut == i)/len(currOut) print(probability) recognizedClass = np.argmax(probability) print("Текст написан:", className[recognizedClass], "с вероятностью", probability[recognizedClass]) [0.0, 0.00558659217877095, 0.002793296089385475, 0.9860335195530726, 0.0, 0.00558659217877095, 0.0] Текст написан: Саймак с вероятностью 0.9860335195530726
В notepad дается функция рассчитывающая вероятность предсказания каждого автора текста. Я не буду приводить код функции, после разбора сделанного выше разобраться по notebook несложно. Результат определения текстов авторов:
О. Генри распознано 100 % сеть считает, что это О. Генри , распознано ВЕРНО! Стругацкие распознано 100 % сеть считает, что это Стругацкие , распознано ВЕРНО! Булгаков распознано 100 % сеть считает, что это Булгаков , распознано ВЕРНО! Саймак распознано 98 % сеть считает, что это Саймак , распознано ВЕРНО! Фрай распознано 84 % сеть считает, что это Фрай , распознано ВЕРНО! Брэдбери распознано 100 % сеть считает, что это Брэдбери , распознано ВЕРНО! Маринина распознано 100 % сеть считает, что это Маринина , распознано ВЕРНО! Средний процент распознавания 95 %
Полезные ссылки
- https://medium.com/starschema-blog/a-comprehensive-guide-to-text-preprocessing-with-python-a47670c5c344 — A comprehensive guide to text pre-processing with python
- https://keras.io/preprocessing/text/
- https://www.datacamp.com/community/tutorials/text-analytics-beginners-nltk — Text Analytics for Beginners using NLTK
- https://machinelearningmastery.com/deep-learning-bag-of-words-model-sentiment-analysis/ — How to Develop a Deep Learning Bag-of-Words Model for Sentiment Analysis (Text Classification)
- https://github.com/adsieg/Multi_Text_Classification
- Классификация текстов с помощью мешка слов. Руководство
- How to Use the Keras Tokenizer
- A Machine Learning Approach to Author Identification of Horror Novels from Text Snippets
- How to Preprocess Character Level Text with Keras
- Automatically Generate Hotel Descriptions with LSTM
- How to Prepare Text Data for Deep Learning with Keras
- Practical Text Classification With Python and Keras
- A Word2Vec Keras tutorial
- https://jobtensor.com/Python-Introduction — Python Tutorial to learn Python