Список работ

Word2Vec в задаче определения тональности текста

Содержание

Введение

Рассматривается задача определения тональности высказываний пользователей интернета средствами нейронной сети (НС) с использованием word2Vec-представления слов корпуса коротких текстов.
НС, получив на входе закодированное высказывание пользователя, должна классифицировать его как позитивное или негативное, то есть решает задачу бинарной классификации (классификации с двумя классами).
Пример позитивного высказывания: "Ура! Мы ломим, гнутся шведы..."
Пример негативного высказывания: "Голод не тетка, пирожка не поднесет."
По жизни, два класса – недостаточно, поскольку могут и нейтральные высказывания, и одновременно и позитивные и негативные, а также с другими интонационными и смысловыми оттенками.
Пример нейтрального высказывания: "Бабушка гадала, надвое сказала."
Пример одновременно и позитивного и негативного высказывания: "Гол как сокол, а остер как топор."
Нейронная сеть обучается на наборе данных, сформированном из файлов positive.csv и negative.csv, скачанных на [1] и содержащих соответственно примеры позитивных (114'911) и негативных (111'923) высказываний (далее предложений).
Эти файлы сформированы в свою очередь на основе корпуса коротких текстов [2], который так же можно скачать на [1] (скачивается файл db.sql). Число записей в корпусе равно 17'639'674. На основе текстов, представленных в этом корпусе, формируется его word2Vec-модель.
При формировании word2Vec-модели используется библиотека gensim. Некоторые методы этой библиотеки для работы с word2Vec-моделью рассмотрены в прил. 1.
Программы формирования набора данных (обучающего и оценочного множеств), составления word2Vec-модели, создания и обучения НС написаны на Python. НС создается с использованием библиотеки Keras. Полный код программы приведен в прил. 4.

Последовательность решения задачи

Формирование word2Vec-представления слов, обучающего и оценочного множеств и создание НС выполняется в следующем порядке:

  1. Создание по файлу db.sql файла data_big.txt с данными для будущего word2Vec-словаря.
  2. Создание отсортированного файла word_big.txt со списком слов текста файла data_big.txt (создается в информационных целях, например, для вычленения слов с ошибками).
  3. Создание word2Vec-представления данных по файлу data_big.txt.
  4. Создание отсортированного файла word_wv*.txt со словами из word2Vec-модели.
  5. Создание файлов pos.txt и neg.txt по файлам positive.csv и negative.csv.
  6. Создание файла data.txt = pos.txt + neg.txt и файла с метками labels.txt.
  7. Создание файла data_n*.txt по файлу data.txt: из data.txt исключаются слова, отсутствующие в word2Vec-модели. Одновременно создается файл labels_n*.txt. Файлы data_n*.txt и labels_n*.txt используются для формирования обучающего и оценочного множеств. Впрочем, их можно формировать и по файлам data.txt и labels.txt.
  8. Создание файла words_mis*.txt со списком слов, имеющихся в data.txt, но отсутствующих в word2Vec-модели (в информационных целях).
  9. Оценка длин предложений (в словах) в файле data.txt (data_n*.txt).
  10. Создание по файлу data.txt (data_n*.txt) файла data_t*.txt, в котором вместо каждого слова стоит его целочисленный уникальный код.
  11. Создание по файлам data_t*.txt и labels*.txt обучающего и оценочного множеств.
  12. Формирование массива e_weights с весами слоя Embedding.
  13. Создание и обучение НС. Оценка качества обучения НС выполняется по точности классификации оценочного множества.

Word2vec-модель и файлы word_wv*.txt, data_n*.txt, labels_n*.txt, data_t*.txt и words_mis*.txt создаются при различных значениях параметра min_count (min_count = 3 или min_count = 5). На место звездочки в именах этих файлов заносятся символы _3 или _5 (в зависимости от значения min_count).
НС обучается либо на множествах, созданных либо по файлам data.txt и labels.txt, если use_data_n = False, либо по файлам data_n*.txt, , labels_n*.txt – в противном случае. Окончание имен файлов обучающего и оценочного множеств формируется в зависимости от значений флажка use_data_n и параметра min_count, например:
trn_x_d_5.bin при use_data_n = False и min_count = 5
или
trn_x_n_3.bin при use_data_n = True и min_count = 3.
Обучающее и оценочное множества хранятся в двоичных файлах.

Параметры программы. Имена файлов

Можно варьировать следующие параметры:

# Данные
# Путь к папке с данными
pathToData = 'G:/AM/НС/poems/tone/'
# Флаг использования файла data_n*.txt, в котором, в отличие от data.txt,
# нет слов, отсутствующих в word2Vec-словаре
use_data_n = True # True False
#
# Метод Word2Vec
# Размер вектора, представляющего слово (размерность пространства word2Vec-модели)
size = 100
# Максимальное расстояние между текущим словом и словами около него
window = 5
# Слово должно встречаться минимум min_count раз, чтобы оно попало в word2Vec-модель
min_count = 3
#
# Модель нейронной сети (НС)
# Размер входа слоя Embedding
# При формировании данных - это максимальное число слов в предложении
# Если в предложении слов больше, то они отбрасываются
max_len = 30
# Флаг использования сверточного слоя Conv1D
use_conv = False
#
# Метод fit
# Размер обучающего пакета
batch = 128
# Число эпох обучения НС
epochs = 20

Имена файлов с данными:

fn_s = pathToData + 'db.sql'
fn_b = pathToData + 'data_big.txt'
fn_w_b = pathToData + 'word_big.txt'
fn_d = pathToData + 'data.txt'
fn_lb = pathToData + 'labels.txt'
m_cnt = '_' + str(min_count)
fn_d_n = pathToData + 'data_n' + m_cnt + '.txt'
fn_lb_n = pathToData + 'labels_n' + m_cnt + '.txt'
fn_w = pathToData + 'word_wv' + m_cnt + '.txt'
fn_m = pathToData + 'words_mis' + m_cnt + '.txt'
suff = '_n' if use_data_n else '_d'
suff_t = suff + m_cnt + '.txt'
suff_b = suff + m_cnt + '.bin'
fn_t = pathToData + 'data_t' + suff_t
fn_trn_x = pathToData + 'trn_x' + suff_b
fn_trn_y = pathToData + 'trn_y' + suff_b
fn_vl_x = pathToData + 'vl_x' + suff_b
fn_vl_y = pathToData + 'vl_y' + suff_b
fn_wv = pathToData + 'word2Vec_' + str(size) + m_cnt + '.model'

Последовательность решения задачи реализуется в результате установки следующих флажков:

# 1. Флаг создания файла data_big.txt по db.sql-файлу с данными для будущего word2Vec-словаря
make_big_data = False # True False
# 2. Флаг создания файла word_big.txt со словами текста из файла data_big.txt
make_word_big = False # True False
# 3. Флаг создания word2Vec-представления данных по файлу data_big.txt
make_word2Vec = False # True False
# 4. Флаг создания отсортированного файла word_wv*.txt со словами из word2Vec-словаря
make_words_wv = False # True False (Время: 10 с)
# 5. Флаг создания файлов pos.txt и neg.txt по файлам positive.csv и negative.csv
make_txt = False # True False
# 6. Флаг создания файла data.txt = pos.txt + neg.txt и файла с метками labels.txt
make_data = False # True False
# 7. Флаг создания файла data_n*.txt по файлу data.txt: из data.txt исключаются
#    предложения, целиком состоящие из слов, отсутствующих в word2Vec-словаре;
#    в оставшихся предложениях исключаются слова, отсутствующие в word2Vec-словаре;
#    так же создается файл words_mis*.txt со списком отсутствующих в word2Vec-словаре слов
#    Одновременно создается файл labels_n*.txt
make_data_n = False # True False (Время: 5 с, работает, если use_data_n = True)
# 8. Флаг оценки длин предложений в словах в наборе данных (файле data_n.txt);
#    так же вычисляется число уникальных слов в наборе данных
sen_len = False # Время: 2 с
# 9. Флаг создания файла data_t*.txt, в котором вместо каждого слова стоит его целочисленный уникальный код
make_data_t = False # True False (Время: 7 с)
# 10. Флаг создания обучающего и оценочного множеств
make_trn_tst = False # True False (Время: 8 с)
# 11. Флаг определения тональности текста
do_tone = True # True False

Общие процедуры и функции

Эти модули вызываются в различных режимах работы.

import numpy as np
def make_dict_w():
    print('Формируем словарь dict_w с записями слово-код слова')
    print('Читаем файл', fn_w)
    word_wv = read_txt_f(fn_w)
    word_wv = [word.replace('\n', '') for word in word_wv]
    dict_w = {}
    k = 0
    for word in word_wv:
        k += 1
        dict_w.update({word: k})
    return dict_w
# Функция preprocess_s выполняет следующее:
# приведение к нижнему регистру: s.lower();
# замена ё на е: s.replace('ё', 'е');
# замена всех символов, кроме русских букв) на пробел: re.sub('[^а-я]+', ' ', s),
# например, вместо
# 'abc + @ № " # $ : % ^ ; ^ & ? * ( ) - _ = + { } [ ] , . / \ ghj федор'
# после lstrip() получим
# 'федор'
# замена поледовательности цифр пробел: re.sub('[\d]+', ' ', s)
# замена нескольких пробелов одним: re.sub(' +', ' ', s);
# удаление ведущих и хвостовых пробелов text.strip().
def preprocess_s(s):
    s = s.lower()
    s = s.replace('ё', 'е')
    s = re.sub('[^а-я]', ' ', s)
    s = re.sub(' +', ' ', s)
    return s.strip()
#
def add_to_txt_f(lst, fn):
    with open(fn, 'w', encoding = 'utf-8') as f:
        for val in lst: f.write((val + '\n') if val.find('\n') == -1 else val)
#
def read_txt_f(fn, encoding = 'utf-8'):
    with open(fn, 'r', encoding = encoding) as f:
        lst = f.readlines() # <class 'list'>
    return lst
#
def add_to_bin_f(arr, fn, dtype = np.uint8):
    with open(fn, 'wb') as f:
        np.array(arr, dtype = dtype).tofile(f)
#
def read_bin_f(fn, reshape, sq = 0, dtype = np.uint8):
    with open(fn, 'rb') as f:
        arr = np.fromfile(f, dtype = dtype)
    if reshape:
        arr_shape0 = int(arr.shape[0] / sq)
        arr = arr.reshape(arr_shape0, sq)
        return arr, arr_shape0
    else:
        return arr

Создание файла data_big.txt с данными для будущей word2Vec-модели

Файл data_big.txt с данными для будущей word2Vec-модели создается по загруженному на [1] файлу db.sql. Этот файл является текстовым содержит SQL-дамп корпуса текста коротких предложений.
Состоит из секций, начинающихся с предложения INSERT INTO `sentiment` VALUES (. Далее следуют заключенные в скобки сообщения, разделенные запятыми. При сообщение переносе из источника в приемник переводится в нижний регистр, далее в сообщении удаляются все символы кроме букв русского алфавита, буква ё заменяется на букву е, удаляются ведущие, двойные и концевые пробелы. В файл-приемник попадают только те сообщения, в которых число слов 3 или более.
Код создания файла data_big.txt:

import re
def one_line(s):
    for m in range(3):
        p = s.find(',')
        s = s[p + 1:]
    s = s[1:]
    p = s.find(',')
    s = s[:p - 1]
    return (preprocess_s(s))
print('Создаем txt-файл с текстом для будущего word2Vec-словаря')
sf = 'INSERT INTO'
sr = 'INSERT INTO `sentiment` VALUES ('
f_b = open(fn_b, mode = 'w', encoding = 'utf-8')
k = 0
with open(fn_s, 'r', encoding = 'utf-8') as f:
    for line in f: # line - <class 'str'>
        if line.find(sf) >= 0:
            k += 1
            if k % 500 == 0: print(k)
            line = line.replace(sr, '')
            lst = line.split('),(')
            for s in lst:
                s = one_line(s)
                t = s.split(' ')
                if len(t) > 2: # Число слов в предложении будет 3 или более
                    f_b.write(s + '\n')
f_b.close()

Число строк в созданном файле равно 13'643'224. Далее из этого файла извлекается список слов.
Всего слов в предложениях файла равно 95'126'793.

Создание файла word_big.txt со списком слов текста файла data_big.txt

Выполняется следующим кодом:

import time
print('Создаем файл', '\n', fn_w_b, '\nсо словами из текста файла data_big.txt')
start_time = time.time() # Время начала создания word_big.txt
f_b = open(fn_b, mode = 'r', encoding = 'utf-8')
dict_w_b = {}
k = 0
for line in f_b:
    k += 1
    if k % 1000000 == 0: print(k)
    lst = line.replace('\n', '').split(' ')
    for w in lst:
        dict_w_b.update({w: 1})
f_b.close()
words = list(dict_w_b.keys())
print('Время создания файла', time.time() - start_time) # 210 c
print('Число строк в файле-источнике', k) # 13'643'224
print('Число слов в файле-приемнике', len(words)) # 1'455'103
words.sort()
add_to_txt_f(words, fn_w_b)
print('Список слов файла data_big.txt записан в файл', fn_w_b)

Всего различных слов в рассматриваемом корпусе равно 1'455'103.
Наибольшая длина слова равна 134, а наименьшая – 1.
Чтобы определить, какие слова написаны с ошибками, загрузим это в файл в таблицу базы данных Microsoft SQL Sever и сравним эту таблицу с таблицей слов русского языка, созданной на основании имеющегося в [3] словаря. Этот словарь содержит 4'159'394 словоформ для 142'792 лемм.
Используемый код:
Создаем таблицу temp:

create table [CrPr].[dbo].[temp] (word char(150))

В файле word_big.txt имеются слова, длина которых существенно превышает размер наибольшую длину слова словаря [3]. Этим и обусловлен размер поля word таблицы temp. (В словаре [3] длина слова не превышает 30.)
Импортируем данные из файла word_big_ansi.txt в таблицу temp (файл word_big_ansi.txt – это копия файла word_big.txt в кодировке ANSI):

bulk insert temp from 'G:\AM\НС\poems\tone\word_big_ansi.txt'
     with (codepage = '1251', rowterminator ='\n')

Выбор слов без ошибок:

select temp.word from temp intersect select all_words_dist.word from all_words_dist

Число слов без ошибок равно 387'078 (из 1'455'103 слов).
Выбор слов с ошибками:

select temp.word from temp except select all_words_dist.word from all_words_dist

Число слов с ошибками равно 1'068'025 (из 1'455'103 слов).
То есть в используемом корпусе слов преобладают неверно написанные слова. Впрочем, многие последовательности символов в этом корпусе навряд ли можно назвать словами, например, 'авмааар' или 'фрннчкк'.
Удаляем таблицу temp:

drop table [CrPr].[dbo].[temp]

Сжимаем базу данных:

dbcc shrinkdatabase ([CrPr])

Создание word2Vec-модели по файлу data_big.txt

В word2Vec-модели из слов корпуса формируются словарь и вектор с координатами каждого слова в линейном пространстве, размерность которого определяется параметром size метода Word2Vec (в примере size = 100).
Разновидности word2Vec-моделей описаны в прил. 2.
Создание word2Vec-модели обеспечивается следующим кодом:

import time
import multiprocessing
from gensim.models import Word2Vec
from gensim.models.word2vec import LineSentence
print('Создание word2Vec-словаря по файлу', fn_b)
print('Читаем файл', fn_b)
data = LineSentence(fn_b) # <class 'gensim.models.word2vec.LineSentence'>
workers = multiprocessing.cpu_count()
print('Создаем word2Vec-словарь; \n size =', size,
      '\n min_count =', min_count, '\n window =', window,
      '\n multiprocessing.cpu_count =', workers)
# sg = 0 - используем модель CBOW (по умолчанию); sg = 1 - используем модель Skip-gram
# size - размерность признакового пространства
# window - максимальное расстояние между текущим словом и словами около него
# min_count - слово должно встречаться минимум min_count раз, чтобы модель его учитывала
start_time = time.time() # Время начала создания word2Vec-словаря
wv_model = Word2Vec(data, size = size, window = window, min_count = min_count,
                 sg = 0, workers = workers)# iter = 5
print('Время создания word2Vec:', (time.time() - start_time))
print('word2vec-модель записана в файл', fn_wv)
wv_model.save(fn_wv)

Использованы два значения параметра min_count.
Число слов в word2Vec-модели при min_count = 3 равно 518'176.
Число слов в word2Vec-модели при min_count = 5 равно 374'061.
Напомним, что всего в рассматриваемом наборе данных 1'455'103 слов (как-бы слов).

Создание файла word_wv*.txt со словами из word2Vec-модели

Выполняется следующим кодом:

from gensim.models import Word2Vec
print('Создаем файл со списком слов word2Vec-словаря\n Берем модель из файла', fn_wv)
wv_model = Word2Vec.load(fn_wv)
wv = wv_model.wv
print('Число слов в словаре:', len(wv.vocab)) # 518'176 / 374'061 - min_count = 3 / 5
print('Создаем список слов словаря word2Vec')
words = wv.index2word
words.sort()
add_to_txt_f(words, fn_w)
print('Список слов word2Vec-словаря записан в файл', fn_w)
print('Число слов в файле-приемнике:', len(words))

Число слов без ошибок в word2Vec-модели при count = 5 равно 201'621, а с ошибками – 172'471.

Создание файлов pos.txt и neg.txt по файлам positive.csv и negative.csv

Выполняется следующим кодом:

import csv
#
def read_csv_save_txt(pathToData, f_csv, f_txt):
    txt0 = []
    fn = pathToData + f_csv
    print('Чтение файла', fn)
    with open(fn, encoding = 'utf-8') as csvDataFile:
        csvReader = csv.reader(csvDataFile, delimiter = ';')
        for row in csvReader:
            t = row[3]
            if len(t) > 1:
                txt0.append(t)
    print('Преобразование прочитанных данных')
    # txt = [preprocess_s(t) for t in txt0] заменяем на цикл с условием len(sen) > 2
    txt = []
    for t in txt0:
        t = preprocess_s(t)
        sen = t.split(' ')
        if len(sen) > 2: # Число слов в предложении будет 3 или более
            txt.append(t)
    fn = pathToData + f_txt
    add_to_txt_f(txt, fn)
    print('Создан файл', fn, 'с числом строк', len(txt))
#
read_csv_save_txt(pathToData, 'positive.csv', 'pos.txt') # 112'984 строки
read_csv_save_txt(pathToData, 'negative.csv', 'neg.txt') # 110'146 строки

При создании файлов исключаются строки, в которых менее трех слов.
Всего в файле pos.txt имеется 112'984 предложений, а neg.txt – 110'146.

Создание файла data.txt = pos.txt + neg.txt и файла с метками в labels.txt

Выполняется следующим кодом:

print('Читаем исходные данные и создаем общий файл с данными и файл с их метками')
pos = read_txt_f(pathToData + 'pos.txt')
neg = read_txt_f(pathToData + 'neg.txt')
lb_pos = ['1'] * len(pos)
lb_neg = ['0'] * len(neg)
data = np.concatenate((pos, neg)) # <class 'numpy.ndarray'>
labels = np.concatenate((lb_pos, lb_neg))
add_to_txt_f(data, fn_d)
add_to_txt_f(labels, fn_lb)
print('Всего примеров:', len(data)) # чуть более 220'000
print('Созданы файлы:\n', fn_d, '\n', fn_lb)

В каждом из полученных файлов по 223'130 строки.
Эти файлы можно использовать для создания обучающего и оценочного множеств. Однако можно предварительно очистить файл data.txt от слов, отсутствующих в word2Vec-модели. Число таких слов при min_count = 3 равно 36'171, а при min_count = 5 – 45'920.

Создание файлов data_n*.txt и labels_n*.txt

Выполняется следующим кодом:

print('Создаем файлы data_n.txt и labels_n.txt по файлам data.txt и labels.txt')
dict_w = make_dict_w()
print('Число слов в словаре:', len(dict_w)) # 518'176 / 374'061 - min_count = 3 / 5
print('Читаем файлы\n', fn_d, '\n', fn_lb)
data = read_txt_f(fn_d) # Число строк: 223'130
labels = read_txt_f(fn_lb)
print('Исключаем слова, отсутствующие в словаре. len(data) =', len(data))
data_n = []
labels_n = []
words_mis = []
num_w = num_w_n = 0
k = -1
for s in data:
    k += 1
    new_s = ''
    s = s.replace('\n', '')
    ls = s.split(' ')
    num_w += len(ls)
    for w in ls:
        if dict_w.get(w) is None:
            words_mis.append(w)
        else:
            num_w_n += 1
            new_s += (' ' + w)
    if len(new_s) > 0:
        data_n.append(new_s.lstrip())
        labels_n.append(labels[k])
print('Число слов в файле-источнике:', num_w) # 2'490'176
# 2'451'501 / 2'439'723 - min_count = 3 / 5
print('Число слов в файле-приемнике:', num_w_n)
# 223'119 / 223'114 - min_count = 3 / 5
print('Число предложений со словами, имеющимися в словаре:', len(data_n))
if len(data_n) > 0:
    add_to_txt_f(data_n, fn_d_n) # 223'119 из 223'130 - min_count = 3
    add_to_txt_f(labels_n, fn_lb_n) # 223'114 из 223'130 - min_count = 5
    print('Предложения со словами, имеющимися в словаре сохранены в файле\n', fn_d_n)
    print('А их метки - в файле\n', fn_lb_n)
words_mis = set(words_mis) # 36'171 / 45'920 - min_count = 3 / 5
words_mis = list(words_mis)
words_mis.sort()
# 36'171 / 45'920 - min_count = 3 / 5
print('Число слов, отсутствующих в словаре:', len(words_mis))
if len(words_mis) > 0:
    add_to_txt_f(words_mis, fn_m)
    print('Слова, отсутствующие в словаре, сохранены в файле\n', fn_m)

Так же по файлу data.txt формируется и файл word_mis*.txt со списком слов, отсутствующих в word2Vec-модели. Число таких слов при min_count = 3 равно 36'171, а при min_count = 5 – 45'920.
Замечание. На месте символа * в именах полученных файлов находятся символы _3 или _5. Число в этих завершающих символах определяется значением параметра min_count.

Оценка длин предложений

Длина предложения вычисляется в словах. Следующим кодом определяется длина самого короткого и длинного предложений, а также средняя длина предложения в заданном текстовом файле:

fn_use = fn_d_n if use_data_n else fn_d
print('Читаем файл', fn_use)
data_use = read_txt_f(fn_use) # encoding = None (для neg.txt и pos.txt)
all_words = []
s_len_avg = s_len_max = 0
s_len_min = max_len
print('Формирование массива слов')
for sen in data_use:
    sen = sen.replace('\n', '')
    lst = sen.split(' ')
    t_len = len(lst)
    s_len_avg += t_len
    if s_len_min == -1: s_len_min = t_len
    if t_len > s_len_max: s_len_max = t_len
    if t_len < s_len_min: s_len_min = t_len
    for word in lst:
        all_words.append(word)
# 2'451'501 / 2'439'723 - min_count = 3 / 5; use_data_n = True
print('Число слов в предложениях набора:', len(all_words))
all_words = set(all_words)
# 132'142 / 122'393 - min_count = 3 / 5; use_data_n = True
print('Число уникальных слов:', len(all_words))
s_len_avg /= len(data_use)
print('s_len_min = ', s_len_min) # 1 / 1 - min_count = 3 / 5
print('s_len_max = ', s_len_max) # 33 / 34 - min_count = 3 / 5
print('s_len_avg = ', int(round(s_len_avg, 0))) # 11 / 11 - min_count = 3 / 5

Полученное значение наибольшего числа слов в предложениях набора (33 и 34 соответственно при min_count = 3 или 5) позволяет взять значение параметра max_len, равным 30.

Создание файла data_t*.txt, в котором вместо слова стоит его код

При создании по файлу data.txt (data_n*.txt) файла data_t*.txt, в котором вместо каждого слова стоит его целочисленный уникальный код, в качестве кода берется номер слова в отсортированном файле word_wv*.txt:

print('Формируем файл data_t.txt')
fn_use = fn_d_n if use_data_n else fn_d
print('Читаем файл', fn_use)
data_use = read_txt_f(fn_use)
# Список data_t повторяет список data_n, но вместо слова стоит код слова = ind + 1,
# где ind - это индекс слова в списке word_wv
dict_w = make_dict_w()
print('Число слов в словаре:', len(dict_w)) # 374'061
print('Число предложений в источнике данных', len(data_use)) # 223'114 / 223'130
print('Замена слов на их коды')
data_t = []
k = 0
start_time = time.time() # Время начала создания data_t.txt
for sen in data_use:
    k += 1
    if k % 50000 == 0: print(k)
    sen = sen.replace('\n', '')
    lst = sen.split(' ')
    new_s = ''
    for word in lst:
        num = dict_w.get(word)
        if num is not None:
            new_s += (' ' + str(num))
    new_s = new_s.lstrip()
    data_t.append(new_s if len(new_s) > 0 else '0')
add_to_txt_f(data_t, fn_t)
print('Сформирован файл с индексами слов:', fn_t)
print('Время создания файлв:', (time.time() - start_time))

Пример соответствующих строк файлов data_n_5.txt и data_t_n_5.txt:
строка 9 файла data_n_5.txt: поприветствуем моего нового читателя;
строка 9 файла data_t_n_5.txt: 236109 163252 186125 358275.
Таким образом, файл data_t*.txt – это закодированный файл data*.txt: слов 'поприветствуем' кодируется числом 236109, 'моего' – 163252 и так далее.
Такая кодировка необходима, поскольку на входе классифицирующей НС должны быть целочисленные данные.

Создание обучающего и оценочного множеств

Выполняется по файлам data_t*.txt и labels*.txt. Окончания имен файлов зависят от значений параметров use_data_n и min_count, например, use_data_n = True и min_count = 5 берутся файлы data_t_n_5.txt и labels_n_5.txt. В результате формируются файлы с именами trn_x_n_5.bin, trn_y_n_5.bin, vl_x_n_5.bin и vl_y_n_5.bin, содержащие соответственно обучающие данные и их метки и оценочные данные и их метки.
При use_data_n = True и min_count = 5 берутся файлы data_t_d_5.txt и labels.txt, а генерируются файлы trn_x_d_5.bin, trn_y_d_5.bin, vl_x_d_5.bin и vl_y_d_5.bin:

from sklearn.model_selection import train_test_split
print('Формируем обучающее и оценочное множества данных')
#
def pad_data_t():
    print('Добавляем нули в конец коротких предложений')
    from keras.preprocessing.sequence import pad_sequences
    data_t_pad = []
    for t in data_t:
        seq = list(map(lambda x: int(x), t.split(' ')))
        data_t_pad.append(seq)
    data_t_pad = pad_sequences(data_t_pad, maxlen = max_len, padding = 'post')
    return data_t_pad
#
fn_lb_use = fn_lb_n if use_data_n else fn_lb
print('Читаем файлы\n', fn_t, '\n', fn_lb_use)
data_t = read_txt_f(fn_t)
labels_use = read_txt_f(fn_lb_use)
labels_use = [int(lb) for lb in labels_use]
data_t_pad = pad_data_t()
test_size = 0.2
print('Разделяем данные на обучающие и оценочные (' + str(test_size) + ')')
x_trn, x_vl, y_trn, y_vl = train_test_split(data_t_pad, labels_use, test_size = test_size, shuffle = True)
add_to_bin_f(x_trn, fn_trn_x, dtype = np.int32)
add_to_bin_f(y_trn, fn_trn_y)
add_to_bin_f(x_vl, fn_vl_x, dtype = np.int32)
add_to_bin_f(y_vl, fn_vl_y)
print('Обучающие и оценочные данные и их метки записаны в файлы\n',
      fn_trn_x, '\n', fn_trn_y, '\n', fn_vl_x, '\n', fn_vl_y)

На вход НС независимо от длины предложения должен подаваться массив формы (max_len, ). Чтобы обеспечить такое постоянство, предложения, число слов в которых менее max_len, пополняются завершающими нулями. Например:

[278089 106964 294431  78749 313086 277577  15842      1  75119 170138
185274 328466 255260 194010 288167      0      0      0      0      0
      0      0      0      0      0      0      0      0      0      0]

Это обеспечивает следующий метод:

data_t_pad = pad_sequences(data_t_pad, maxlen = max_len, padding = 'post')

Кроме того, ранее при создании файла data.txt длина предложения была ограничена max_len.

Формирование массива с весами слоя Embedding. Создание и обучение нейронной сети

В используемой НС после входного слоя находится слой Embedding, на входе которого закодированное предложение (см. пред. разд.), а на выходе – массив с координатами слов предложения в сформированной ранее word2Vec-модели корпуса слов. Чтобы обеспечить такой результат выполняется следующее:

В принципе, можно сделать модель НС и без слоя Embedding и последующего слоя Flatten, если сразу подавать на слой Dropout массив с координатами слов формы (None, 3000) (при обучении None принимает значение batch_size).
Формирование массива e_weights с весами слоя Embedding, создание и обучение НС обеспечивает следующий код:

import time
from gensim.models import Word2Vec
from keras.models import Model
from keras.layers.embeddings import Embedding
from keras.layers import Input, Flatten, Dense, Dropout
if use_conv:
    from keras.layers.convolutional import Conv1D
    from keras.layers.pooling import GlobalMaxPooling1D
#
def make_e_weights():
    print('Формируем массив с весами слоя Embedding')
    e_weights = np.zeros((num_words, size))
    for w, t in dict_w.items(): # Слово и его код
        w_coords = dict_w2v.get(w) # Координаты слова
        if w_coords is not None:
            e_weights[t] = w_coords
    return e_weights
dict_w = make_dict_w()
num_words = len(dict_w) + 1
print('Читаем файл', fn_wv)
wv_model = Word2Vec.load(fn_wv)
wv = wv_model.wv
# wv.index2word - список слов словаря
# wv.syn0 - массив координат слов
dict_w2v = dict(zip(wv.index2word, wv.syn0))
e_weights = make_e_weights()
#
print('Загрузка обучающего и оценочного множеств из файлов\n', fn_trn_x,
      '\n', fn_trn_y, '\n', fn_vl_x, '\n', fn_vl_y)
x_trn, _ = read_bin_f(fn_trn_x, True, sq = max_len, dtype = np.int32)
x_vl, _ = read_bin_f(fn_vl_x, True, sq = max_len, dtype = np.int32)
y_trn = read_bin_f(fn_trn_y, False)
y_vl = read_bin_f(fn_vl_y, False)
#
print('Формируем модель')
inp = Input(shape = (max_len, ), dtype = 'int32')
# Слой по коду слова выдает координаты слова в e_weights (word2Vec-словаре)
x = Embedding(num_words, output_dim = size, input_length = max_len,
              weights = [e_weights], trainable = False)(inp)
if use_conv:
    x = Dropout(0.5)(x)
    x = Conv1D(filters = 3, kernel_size = 3, padding = 'same', activation = 'relu')(x)
x = Flatten()(x)
x = Dropout(0.3)(x)
x = Dense(16, activation = 'relu')(x)
output = Dense(1, activation = 'sigmoid')(x)
model = Model(inp, output)
model.summary()
print('Обучаем модель')
print('Используем', ('сжатые' if use_data_n else 'полные'), 'данные')
model.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy'])
start_time = time.time() # Время начала обучения
model.fit(x_trn, y_trn, batch_size = batch, epochs = epochs,
           verbose = 2, validation_data = (x_vl, y_vl))
print('Время обучения:', time.time() - start_time)

Описание модели НС:

Слой (тип) Форма выхода e Число параметров
input_1 (InputLayer)(None, 30)0
embedding_1 (Embedding)(None, 30, 100)37'406'200
flatten_1 (Flatten)(None, 3000)0
dropout_1 (Dropout)(None, 3000)0
dense_1 (Dense)(None, 16)48'016
dense_2 (Dense)(None, 1)17
Всего параметров: 37'454'233
Обучаемых параметров: 48'033
Необучаемых параметров: 37'406'200

При использовании метода оптимизации Adam, функции потерь binary_crossentropy, batch_size = 128 и epochs = 20 получена точность классификации оценочного множества, равная:

Прочие параметры, использованные при создании word2vec-модели: size = 100; window = 5; sg = 0; iter = 5.
Заметим, что при случайной инициализации весов слоя Embedding с использованием RandomNormal достигается точность классификации, равная 0.6075.

Заключение

Низкая точность классификации объясняется несколькими факторами. Прежде всего – это большое число грамматических ошибок в текстах корпуса: из 1'455'103 уникальных слов, имеющихся в корпусе, 1'068'025 написаны с ошибками или являются просторечиями. По этой причине в word2Vec-модель при min_count = 3 попало 518'176 слов, а при min_count = 5 – только 374'061. То есть большая часть слов корпуса оказалась исключенной из процесса классификации. Во-вторых, бинарная классификация высказываний является неполной, поскольку, как уже отмечалось выше, высказывание может быть не только позитивным или негативным, но и иметь иной оттенок, например, нейтральный. Пути преодоления этих обстоятельств очевидны: автоматическое исправление ошибок, не меняющее интонацию высказывания, и увеличение числа классов. В то же время можно попытаться улучшить ситуацию, предприняв следующие действия:

Приложение 1. Некоторые методы gensim Word2Vec

Прежде указываем используемые библиотеки:

import numpy as np
from gensim.models import Word2Vec
from gensim.models.word2vec import LineSentence

Подготовка данных по тексту в текстовом файле:

data = LineSentence(source = fn_b, max_sentence_length = 150, limit = None)
print(data.limit)
print(data.max_sentence_length)
print(data.source)

Формирование модели обеспечивает метод Word2Vec.

wv_model = Word2Vec(...)

Запись модели в файл:

wv_model.save(имя_файла)

Загрузка модели из файла:

wv_model = Word2Vec.load(имя_файла)

Далее приводится код, в котором иллюстрируются некоторые свойства и методы word2Vec-модели:

wv_model = Word2Vec.load(fn_wv)
#
# Число строк в корпусе
print(wv_model.corpus_count) # 13'643'224
#
# Всего слов в корпусе
print(wv_model.corpus_total_words) # 95'126'793
#
# wv – индексированные векторы слов
wv = wv_model.wv
print(type(wv)) # <class 'gensim.models.keyedvectors.Word2VecKeyedVectors'>
#
# Словарь модели
vocab = wv.vocab
#
# Число слов в словаре модели
n_words_wv = len(vocab)
print(n_words_wv) # 374'061
#
# Слова для последующего использования
word = 'мудро'
word2 = 'оценка'
word3 = 'лот'
#
# Координаты заданного слова
print(wv[word])
# или
print(wv.get_vector(word))
#
# Расстояние между словами
print(wv.distance(word, word2)) # 1.06
print(wv.distance(word2, word3)) # 0.82
#
# Расстояния между словом и словами из последующего кортежа
print(wv.distances(word, (word, word2, word3))) # [0. 1.0596781 1.1261251]
print(wv.distances(word)) # Расстояния между словом и всеми прочими словами
#
# Слова, наиболее близкие к заданному слову
sims = wv_model.similar_by_word(word)
print(sims)
# [('примитивно', 0.6062056422233582), ('хитро', 0.589867115020752),
# ('умно', 0.5441082715988159), ('глуп', 0.5423044562339783),
# ('разумно', 0.5420589447021484), ('продумано', 0.5388686656951904),
# ('трагично', 0.5290220975875854), ('банально', 0.5251309275627136),
# ('гармонично', 0.5153691172599792), ('лаконично', 0.5152344107627869)]
#
# Слова, наиболее близкие к заданному вектору
vec = wv.get_vector(word)
sims = wv_model.similar_by_vector(vec)
print(sims)
# [('мудро', 1.0), ('примитивно', 0.6062056422233582), ('хитро', 0.589867115020752),
# ('умно', 0.5441082715988159), ('глуп', 0.5423044562339783),
# ('разумно', 0.5420589447021484), ('продумано', 0.5388686656951904),
# ('трагично', 0.5290220975875854), ('банально', 0.5251308679580688),
# ('гармонично', 0.5153691172599792)]
#
# Слова, наиболее близкие к вектору -vec
sims = wv_model.similar_by_vector(-vec)
print(sims)
#
# Слова, наиболее похожие к разнице двух слов
# бутерброд – сыр
m_sim = wv.most_similar(positive = ['бутерброд'], negative = ['сыр'])
print(m_sim) # Хотелось бы получить 'хлеб', но не выходит
#
# Слова, наиболее похожие к сумме двух слов
# хлеб + сыр
m_sim = wv.most_similar(positive = ['хлеб', 'сыр'], negative = [])
print(m_sim) # Хотелось бы получить 'бутерброд', но имеем:
#[('коньяк', 0.7822180986404419), ('творог', 0.7667460441589355),
# ('квас', 0.7576421499252319), ('сок', 0.7569496631622314),
# ('майонез', 0.7530159950256348), ('виноград', 0.7483903169631958),
# ('соус', 0.7479187250137329), ('кетчуп', 0.7448550462722778),
# ('рис', 0.7432571649551392), ('йогурт', 0.743067741394043)]
#
# хлеб + колбаса
m_sim = wv.most_similar(positive = [['хлеб', 'колбаса'], negative = [])
print(m_sim) # Хотелось бы получить 'бутерброд', но имеем:
#[('капуста', 0.7554473876953125), ('кетчуп', 0.7481403350830078),
# ('творог', 0.7452255487442017), ('майонез', 0.7342149019241333),
# ('сгущенка', 0.7321579456329346), ('сметана', 0.7289685010910034),
# ('рис', 0.7288397550582886), ('брокколи', 0.7266126871109009),
# ('соус', 0.7255753874778748), ('гречка', 0.7229445576667786)]
#
# Слова, противоположные слову 'собака'
m_sim = wv.most_similar(positive = [], negative = ['собака'])
print(m_sim) # Хотелось бы получить кошка', но не выходит
#
# Список слов словаря
words_list = list(vocab.keys())
# или
words_list = wv.index2word
#
# Список координат слов
vec_list = list(vocab.values())
# или
vec_list = wv.syn0
# или
vec_list = wv.vectors
#
# Прогноз центрального слова, заданного списком слов контекста
output_word = wv_model.predict_output_word(['испугалась', 'когда', 'он', 'пришел', 'ко', 'мне'], topn = 5)
print(output_word)
#
# Вывод трех случайно выбранных слов и их координат
for k in range(3):
    n = np.random.randint(n_words_wv - 1)
    word = words_list[n]
    vec = vec_list[n]
    print(word)
    print(vec)

Больше информации см. в других источниках.

Приложение 2. word2vec-модели

Виды моделей word2vec

Word2vec – это один из инструментов распределенного представления слов [4]. Идея word2vec состоит в том, чтобы научиться предсказывать слово из его контекста (модель непрерывного мешка CBOW) или наоборот – контекст по слову (модель Skip-gram).
Word2vec-модель строится исходя из дистрибутивной гипотезы [5], предполагающей, что слова с похожим смыслом встречаются похожих контекстах. В модели каждое слово представляется вещественным вектором (word embedding) заданной длины. При формировании модели в результате анализа окружения слова (контекста) близкие по значению слова (с точки зрения модели) представляются векторами, расстояние между которыми незначительно, и наоборот, слова, существующие в несовпадающих контекстах, представляются векторами, расстояние между которыми весьма велико.
Иными словами, выполняется максимизация близости векторов слов, которые появляются рядом друг с другом, и минимизация близости векторов слов, которые не появляются друг рядом с другом. Оценку расстояния между векторами можно выполнять, например, используя косинусную близость.
Суть модели CBOW заключается в том, чтобы научиться как можно лучше решать следующую задачу: по заданному контексту слова восстановить само слово. Предсказывается слово по левому и правому контекстам (рис. 1).
Модель Skip-gram работает противоположным образом: пытается предсказать каждое слово контекста по данному слову (рис. 1).

CBOW и Skip-gram

Рис. 1. Модели CBOW и Skip-gram

В обеих моделях размер окна контекста равен 2 (window = 2). Число слов в контексте будет меньше, если, например, обрабатывается первое или последнее слово документа (в качестве документа может выступать, например, абзац текста или отдельное предложение).
Создание word2Vec-модели может состоять из следующих этапов:

В случае CBOW обучающее множество совпадает с C, а множество меток – с M. В случае Skip-gram все ровно наоборот.
Предсказание слова по контексту или контекста по слову выполняется нейронной сетью с (НС) одним скрытым слоем (рис. 2), имеющим линейную функцию активации – это означает, что при вычислении выхода нейрона скрытого слоя усредняются поступившие на нейрон сигналы.

CBOW и Skip-gram

Рис. 2. Сруктуры нейронных сетей для получения CBOW- и Skip-gram-моделей

В обеих НС число нейронов на скрытом слое равно size – размеру вектора, представляющего слово (размерность пространства word2Vec-модели). Длина массива W равна nV. Выход скрытого слоя – это массив W' формы (nV, size). После завершения процесса обучения нейронной сети i-я строка массива W' рассматривается как вектор, представляющий слово wi словаря V.

Обучение нейронных сетей

В случае CBOW перебираются массивы множества C, и выбранный массив Ci – контекст слова wi подается на вход НС. Таким образом, на вход НС поступает h * nV сигналов xiCi.
Выходной слой НС с функцией активации softmax содержит nV нейронов. При вычислении ошибки берутся прогноз НС – вектор y_pred и вектор y_true, имеющий, если 1 в позиции i и нули во всех прочих позициях. Для вычисления потерь можно использовать перекрестную энтропию.

В качестве целевой функции используется общее правдоподобие корпуса текста. Значение правдоподобия максимизируется. Логарифм целевой функции записывается следующим образом:

CBOW функция правдоподобия,

где θ – параметры модели;
w – слово;
c – локальный контекст слова;
V – словарь;
C – множество локальных контекстов.
В случае Skip-gram имеем следующий логарифм целевой функции:

Skip-gram функция правдоподобия.

Вероятность моделируется функцией softmax.
Для уменьшения вычислительной сложности в обеих моделях используется либо иерархический softmax, либо отрицательное сэмплирование, а для повышения точности часто употребляемые слова изымаются в Skip-gram модели из обучающего множества с вероятностью, зависящей от частоты присутствия слова в корпусе текста [6].

Приложение 3. Некоторые недостатки Word2Vec-модели

Word2Vec-модель пытается отразить семантические взаимосвязи слов в выбранном корпусе текста, и при наличии соответствующего корпуса делает это весьма успешно. В то же время она имеет ряд недостатков, например, следующие:

  1. Пропускает много слов, если min_count > 0.
  2. Плохо позиционирует редко встречаемые слова (появляются в словаре при низких значениях min_count).
  3. При изменении состава корпуса текста меняются координаты слов.
  4. При изменении параметров модели, например, size или window меняются отношения между векторами, представляющими слова.
  5. Не различает омонимы.

Омонимы – это слова, совпадающее с по написанию, но разные по значению.

Примеры.

Некоторые омонимы слова коса:

Некоторые омонимы слова косой:

Приложение 4. Полный код программы

import numpy as np
#
# Данные
# Путь к папке с данными
pathToData = 'G:/AM/НС/poems/tone/'
# Флаг использования файла data_n*.txt, в котором, в отличие от data.txt,
# нет слов, отсутствующих в word2Vec-словаре
use_data_n = True # True False
#
# Метод Word2Vec
# Размер вектора, представляющего слово (размерность пространства word2Vec)
size = 100
# Максимальное расстояние между текущим словом и словами около него (окно контекста)
window = 5
# Слово должно встречаться минимум min_count раз, чтобы оно попало в word2Vec-модель
min_count = 5
#
# Модель нейронной сети (НС)
# Размер входа слоя Embedding
# При формировании данных - это максимальное число слов в предложении
# Если в предложении слов больше, то они отбрасываются
max_len = 30
# Флаг использования сверточного слоя Conv1D
use_conv = False
#
# Метод fit
# Размер обучающего пакета
batch = 128
# Число эпох обучения НС
epochs = 20
#
# Имена файлов
fn_s = pathToData + 'db.sql'
fn_b = pathToData + 'data_big.txt'
fn_w_b = pathToData + 'word_big.txt'
fn_d = pathToData + 'data.txt'
fn_lb = pathToData + 'labels.txt'
m_cnt = '_' + str(min_count)
fn_d_n = pathToData + 'data_n' + m_cnt + '.txt'
fn_lb_n = pathToData + 'labels_n' + m_cnt + '.txt'
fn_w = pathToData + 'word_wv' + m_cnt + '.txt'
fn_m = pathToData + 'words_mis' + m_cnt + '.txt'
suff = '_n' if use_data_n else '_d'
suff_t = suff + m_cnt + '.txt'
suff_b = suff + m_cnt + '.bin'
fn_t = pathToData + 'data_t' + suff_t
fn_trn_x = pathToData + 'trn_x' + suff_b
fn_trn_y = pathToData + 'trn_y' + suff_b
fn_vl_x = pathToData + 'vl_x' + suff_b
fn_vl_y = pathToData + 'vl_y' + suff_b
fn_wv = pathToData + 'word2Vec_' + str(size) + m_cnt + '.model'
#
# Флажки
# 1. Флаг создания файла data_big.txt по db.sql-файлу с данными для будущего word2Vec-словаря
make_big_data = False # True False
#
# 2. Флаг создания файла word_big.txt со словами текста из файла data_big.txt
make_word_big = False # True False
#
# 3. Флаг создания word2Vec-представления данных по файлу data_big.txt
make_word2Vec = False # True False
#
# 4. Флаг создания отсортированного файла word_wv*.txt со словами из word2Vec-словаря
make_words_wv = False # True False (Время: 10 с)
#
# 5. Флаг создания файлов pos.txt и neg.txt по файлам positive.csv и negative.csv
make_txt = False # True False
#
# 6. Флаг создания файла data.txt = pos.txt + neg.txt и файла с метками labels.txt
make_data = False # True False
#
# 7. Флаг создания файла data_n*.txt по файлу data.txt: из data.txt исключаются
#    предложения, целиком состоящие из слов, отсутствующих в word2Vec-словаре;
#    в оставшихся предложениях исключаются слова, отсутствующие в word2Vec-словаре;
#    так же создается файл words_mis*.txt со списком отсутствующих в word2Vec-словаре слов
#    Одновременно создается файл labels_n*.txt
make_data_n = False # True False (Время: 5 с, работает, если use_data_n = True)
#
# 8. Флаг оценки длин предложений в словах в наборе данных (файле data_n.txt);
#    так же вычисляется число уникальных слов в наборе данных
sen_len = False # Время: 2 с
#
# 9. Флаг создания файла data_t*.txt, в котором вместо каждого слова стоит его целочисленный уникальный код
make_data_t = False # True False (Время: 7 с)
#
# 10. Флаг создания обучающего и оценочного множеств
make_trn_tst = False # True False (Время: 8 с)
show_trn_tst = False
#
# 11. Флаг определения тональности текста
do_tone = True # True False
predict_emb = False # Флаг вывода прогноза после слоя Embedding
# Проверяем, если predict_emb = True, действительно ли Embedding-слой
# по коду слова выдает координаты слова в e_weights (word2Vec-словаре)
# Флаг использования в слое Embedding случайно сгенерированных весов
rand_weights = False
#
# Не используется
do_token = False # Флаг токенизации данных - не используется
read_data_t = False # Флаг чтения токенизированных данных
#
def make_dict_w():
    print('Формируем словарь dict_w с записями слово-код слова')
    print('Читаем файл', fn_w)
    word_wv = read_txt_f(fn_w)
    word_wv = [word.replace('\n', '') for word in word_wv]
    dict_w = {}
    k = 0
    for word in word_wv:
        k += 1
        dict_w.update({word: k})
##    print(list(dict_w.items())[len(word_wv) - 1])
    return dict_w
# Функция preprocess_s выполняет следующее:
# приведение к нижнему регистру: s.lower();
# замена ё на е: s.replace('ё', 'е');
# замена ссылок, например http://t.co/qjlwirfD3A, на пробел;
# замена имени пользователя, например @first_timee, на пробел: re.sub('@[^\s]+', ' ', s) ;
# замена не букв (кроме цифр) на пробел: re.sub('[^a-zA-Zа-яА-Я1-9]+', ' ', s),
# замена всех символов, кроме русских букв) на пробел: re.sub('[^а-я]+', ' ', s),
# например, вместо
# 'abc + @ № " # $ : % ^ ; ^ & ? * ( ) - _ = + { } [ ] , . / \ ghj федор'
# после lstrip() получим
# 'федор'
# замена поледовательности цифр пробел: re.sub('[\d]+', ' ', s)
# замена rt на пустой символ;
# замена нескольких пробелов одним: re.sub(' +', ' ', s);
# удаление ведущих и хвостовых пробелов text.strip().
def preprocess_s(s):
    s = s.lower()
##    s = re.sub('((www\.[^\s]+)|(https?://[^\s]+))', ' ', s)
##    s = re.sub('@[^\s]+', ' ', s)
    s = re.sub('[^а-я]', ' ', s)
    s = re.sub(' +', ' ', s)
    s = s.replace('ё', 'е')
    return s.strip()
#
def add_to_txt_f(lst, fn):
    with open(fn, 'w', encoding = 'utf-8') as f:
        for val in lst: f.write((val + '\n') if val.find('\n') == -1 else val)
#
def read_txt_f(fn, encoding = 'utf-8'):
    with open(fn, 'r', encoding = encoding) as f:
        lst = f.readlines() # <class 'list'>
    return lst
#
def add_to_bin_f(arr, fn, dtype = np.uint8):
    with open(fn, 'wb') as f:
        np.array(arr, dtype = dtype).tofile(f)
#
def read_bin_f(fn, reshape, sq = 0, dtype = np.uint8):
    with open(fn, 'rb') as f:
        arr = np.fromfile(f, dtype = dtype)
    if reshape:
        arr_shape0 = int(arr.shape[0] / sq)
        arr = arr.reshape(arr_shape0, sq)
        return arr, arr_shape0
    else:
        return arr
#
if make_big_data:
    import re
    def one_line(s):
        for m in range(3):
            p = s.find(',')
            s = s[p + 1:]
        s = s[1:]
        p = s.find(',')
        s = s[:p - 1]
        return (preprocess_s(s))
    print('Создаем txt-файл с текстом для будущего word2Vec-словаря')
    sf = 'INSERT INTO'
    sr = 'INSERT INTO `sentiment` VALUES ('
    f_b = open(fn_b, mode = 'w', encoding = 'utf-8')
    k = 0
    with open(fn_s, 'r', encoding = 'utf-8') as f:
        for line in f: # line - <class 'str'>
            if line.find(sf) >= 0:
                k += 1
                if k % 500 == 0: print(k)
                line = line.replace(sr, '')
                lst = line.split('),(')
                for s in lst:
                    s = one_line(s)
                    t = s.split(' ')
                    if len(t) > 2: # Число слов в предложении будет 3 или более
                        f_b.write(s + '\n')
    f_b.close()
#
elif make_word_big:
    import time
    print('Создаем файл', '\n', fn_w_b, '\nсо словами из текста файла data_big.txt')
    start_time = time.time() # Время начала создания word_big.txt
    f_b = open(fn_b, mode = 'r', encoding = 'utf-8')
    dict_w_b = {}
    k = len_max =0
    len_min = 30
    for line in f_b:
        k += 1
        if k % 1000000 == 0: print(k)
        lst = line.replace('\n', '').split(' ')
        for w in lst:
            len_w = len(w)
            if len_w > len_max: len_max = len_w
            if len_w < len_min: len_min = len_w
            dict_w_b.update({w: 1})
    f_b.close()
    words = list(dict_w_b.keys())
    print('Время создания файла', time.time() - start_time) # 210 c
    print('Число строк в файле-источнике', k) # 13'643'224
    print('Число слов в файле-приемнике', len(words)) # 1'455'103
    print('Длина слова. \nМинимальная:', len_min, '\nМаксимальная:', len_max)
    words.sort()
    add_to_txt_f(words, fn_w_b)
    print('Список слов файла data_big.txt записан в файл', fn_w_b)
#
elif make_word2Vec:
    import time
    import multiprocessing
    from gensim.models import Word2Vec
    from gensim.models.word2vec import LineSentence
##    import logging
##    logging.basicConfig(format = '%(asctime)s : %(levelname)s : %(message)s', level = logging.INFO)
    print('Создание word2Vec-словаря по файлу', fn_b)
    print('Читаем файл', fn_b)
    data = LineSentence(fn_b) # <class 'gensim.models.word2vec.LineSentence'>
    workers = multiprocessing.cpu_count()
    print('Создаем word2Vec-словарь; \n size =', size,
          '\n min_count =', min_count, '\n window =', window,
          '\n multiprocessing.cpu_count =', workers)
    # sg = 0 - используем модель CBOW (по умолчанию); sg = 1 - используем модель Skip-gram
    # size - размерность признакового пространства
    # window - максимальное расстояние между текущим словом и словами около него
    # min_count - слово должно встречаться минимум min_count раз, чтобы модель его учитывала
    start_time = time.time() # Время начала создания word2Vec-словаря
    wv_model = Word2Vec(data, size = size, window = window, min_count = min_count,
                     sg = 0, workers = workers) # iter = 5 (по умолчанию)
    print('Время создания word2Vec:', (time.time() - start_time))
    print('Word2vec-модель записана в файл', fn_wv)
    wv_model.save(fn_wv)
elif make_words_wv:
    from gensim.models import Word2Vec
    print('Создаем файл со списком слов word2Vec-словаря\n Берем модель из файла', fn_wv)
    wv_model = Word2Vec.load(fn_wv)
    wv = wv_model.wv
    print('Число слов в словаре:', len(wv.vocab)) # 518'176 / 374'061 - min_count = 3 / 5
    print('Создаем список слов словаря word2Vec')
    words = wv.index2word
    words.sort()
    add_to_txt_f(words, fn_w)
    print('Список слов word2Vec-словаря записан в файл', fn_w)
    print('Число слов в файле-приемнике:', len(words))
#
elif make_txt:
    import csv
    #
    def read_csv_save_txt(pathToData, f_csv, f_txt):
        txt0 = []
        fn = pathToData + f_csv
        print('Чтение файла', fn)
        with open(fn, encoding = 'utf-8') as csvDataFile:
            csvReader = csv.reader(csvDataFile, delimiter = ';')
            for row in csvReader:
                t = row[3]
                if len(t) > 1:
                    txt0.append(t)
        print('Преобразование прочитанных данных')
        # txt = [preprocess_s(t) for t in txt0] заменяем на цикл с условием len(sen) > 2
        txt = []
        for t in txt0:
            t = preprocess_s(t)
            sen = t.split(' ')
            if len(sen) > 2: # Число слов в предложении будет 3 или более
                txt.append(t)
        fn = pathToData + f_txt
        add_to_txt_f(txt, fn)
        print('Создан файл', fn, 'с числом строк', len(txt))
    #
    read_csv_save_txt(pathToData, 'positive.csv', 'pos.txt') # 112'984 строки
    read_csv_save_txt(pathToData, 'negative.csv', 'neg.txt') # 110'146 строки
elif make_data:
    print('Читаем исходные данные и создаем общий файл с данными и файл с их метками')
    pos = read_txt_f(pathToData + 'pos.txt')
    neg = read_txt_f(pathToData + 'neg.txt')
    lb_pos = ['1'] * len(pos)
    lb_neg = ['0'] * len(neg)
    data = np.concatenate((pos, neg)) # <class 'numpy.ndarray'>
    labels = np.concatenate((lb_pos, lb_neg))
    add_to_txt_f(data, fn_d)
    add_to_txt_f(labels, fn_lb)
    print('Всего примеров:', len(data)) # чуть более 220'000
    print('Созданы файлы:\n', fn_d, '\n', fn_lb)
#
elif make_data_n:
    if not use_data_n:
        import sys
        print('Использование файла data_n.txt запрещено. use_data_n = False')
        sys.exit()
    print('Создаем файлы data_n.txt и labels_n.txt по файлам data.txt и labels.txt')
    dict_w = make_dict_w()
    print('Число слов в словаре:', len(dict_w)) # 518'176 / 374'061 - min_count = 3 / 5
    print('Читаем файлы\n', fn_d, '\n', fn_lb)
    data = read_txt_f(fn_d) # Число строк: 223'130
    labels = read_txt_f(fn_lb)
    print('Исключаем слова, отсутствующие в словаре. len(data) =', len(data))
    data_n = []
    labels_n = []
    words_mis = []
    num_w = num_w_n = 0
    k = -1
    for s in data:
        k += 1
        new_s = ''
        s = s.replace('\n', '')
        ls = s.split(' ')
        num_w += len(ls)
        for w in ls:
            if dict_w.get(w) is None:
                words_mis.append(w)
            else:
                num_w_n += 1
                new_s += (' ' + w)
        if len(new_s) > 0:
            data_n.append(new_s.lstrip())
            labels_n.append(labels[k])
    print('Число слов в файле-источнике:', num_w) # 2'490'176
    # 2'451'501 / 2'439'723 - min_count = 3 / 5
    print('Число слов в файле-приемнике:', num_w_n)
    # 223'119 / 223'114 - min_count = 3 / 5
    print('Число предложений со словами, имеющимися в словаре:', len(data_n))
    if len(data_n) > 0:
        add_to_txt_f(data_n, fn_d_n) # 223'119 из 223'130 - min_count = 3
        add_to_txt_f(labels_n, fn_lb_n) # 223'114 из 223'130 - min_count = 5
        print('Предложения со словами, имеющимися в словаре сохранены в файле\n', fn_d_n)
        print('А их метки - в файле\n', fn_lb_n)
    words_mis = set(words_mis) # 36'171 / 45'920 - min_count = 3 / 5
    words_mis = list(words_mis)
    words_mis.sort()
    # 36'171 / 45'920 - min_count = 3 / 5
    print('Число слов, отсутствующих в словаре:', len(words_mis))
    if len(words_mis) > 0:
        add_to_txt_f(words_mis, fn_m)
        print('Слова, отсутствующие в словаре, сохранены в файле\n', fn_m)
#
elif sen_len:
##    from nltk.tokenize import word_tokenize
    fn_use = fn_d_n if use_data_n else fn_d
    print('Читаем файл', fn_use)
    data_use = read_txt_f(fn_use) # encoding = None (для neg.txt и pos.txt)
    all_words = []
    s_len_avg = s_len_max = 0
    s_len_min = max_len
    print('Формирование массива слов')
    for sen in data_use:
        sen = sen.replace('\n', '')
##        tokenize_word = word_tokenize(sen)
##        t_len = len(tokenize_word)
        lst = sen.split(' ')
        t_len = len(lst)
        s_len_avg += t_len
        if s_len_min == -1: s_len_min = t_len
        if t_len > s_len_max: s_len_max = t_len
        if t_len < s_len_min: s_len_min = t_len
##        for word in tokenize_word:
        for word in lst:
            all_words.append(word)
    # 2'451'501 / 2'439'723 - min_count = 3 / 5; use_data_n = True
    print('Число слов в предложениях набора:', len(all_words))
    all_words = set(all_words)
    # 132'142 / 122'393 - min_count = 3 / 5; use_data_n = True
    print('Число уникальных слов:', len(all_words))
    s_len_avg /= len(data_use)
    print('s_len_min = ', s_len_min) # 1 / 1 - min_count = 3 / 5
    print('s_len_max = ', s_len_max) # 33 / 34 - min_count = 3 / 5
    print('s_len_avg = ', int(round(s_len_avg, 0))) # 11 / 11 - min_count = 3 / 5
#
elif make_data_t:
    import time
    print('Формируем файл data_t.txt')
    fn_use = fn_d_n if use_data_n else fn_d
    print('Читаем файл', fn_use)
    data_use = read_txt_f(fn_use)
    # Список data_t повторяет список data_n, но вместо слова стоит код слова = ind + 1,
    # где ind - это индекс слова в списке word_wv
    dict_w = make_dict_w()
    print('Число слов в словаре:', len(dict_w)) # 374'061
    print('Число предложений в источнике данных', len(data_use)) # 223'114 / 223'130
    print('Замена слов на их коды')
    data_t = []
    k = 0
    start_time = time.time() # Время начала создания data_t.txt
    for sen in data_use:
        k += 1
        if k % 50000 == 0: print(k)
        sen = sen.replace('\n', '')
        lst = sen.split(' ')
        new_s = ''
        for word in lst:
            num = dict_w.get(word)
            if num is not None:
                new_s += (' ' + str(num))
        new_s = new_s.lstrip()
        data_t.append(new_s if len(new_s) > 0 else '0')
    add_to_txt_f(data_t, fn_t)
    print('Сформирован файл с индексами слов:', fn_t)
    print('Время создания файлв:', (time.time() - start_time))
#
elif make_trn_tst:
    if show_trn_tst:
        def show_ex(n, x, y, len_xy):
            for k in range(n):
                n_xy = np.random.randint(len_xy - 1)
                print('n_xy =', n_xy, '\nx =', x[n_xy], '\ny =', y_trn[n_xy])
        print('Показываем примеры обучающего и оценочного множества данных')
        print('Загрузка обучающего и оценочного множеств из файлов\n', fn_trn_x,
              '\n', fn_trn_y, '\n', fn_vl_x, '\n', fn_vl_y)
        x_trn, len_trn = read_bin_f(fn_trn_x, True, sq = max_len, dtype = np.int32)
        x_vl, len_vl = read_bin_f(fn_vl_x, True, sq = max_len, dtype = np.int32)
        y_trn = read_bin_f(fn_trn_y, False)
        y_vl = read_bin_f(fn_vl_y, False)
        n = 3
        print('Примеры обучающего множества'); show_ex(n, x_trn, y_trn, len_trn)
        print('Примеры проверочного множества'); show_ex(n, x_vl, y_vl, len_vl)
    else:
        from sklearn.model_selection import train_test_split
        print('Формируем обучающее и оценочное множества данных')
        #
        def pad_data_t():
            print('Добавляем нули в конец коротких предложений')
            from keras.preprocessing.sequence import pad_sequences
            data_t_pad = []
            for t in data_t:
                seq = list(map(lambda x: int(x), t.split(' ')))
                data_t_pad.append(seq)
            data_t_pad = pad_sequences(data_t_pad, maxlen = max_len, padding = 'post')
            return data_t_pad
        #
        fn_lb_use = fn_lb_n if use_data_n else fn_lb
        print('Читаем файлы\n', fn_t, '\n', fn_lb_use)
        data_t = read_txt_f(fn_t)
        labels_use = read_txt_f(fn_lb_use)
        labels_use = [int(lb) for lb in labels_use]
        data_t_pad = pad_data_t()
        test_size = 0.2
        print('Разделяем данные на обучающие и оценочные (' + str(test_size) + ')')
        x_trn, x_vl, y_trn, y_vl = train_test_split(data_t_pad, labels_use, test_size = test_size, shuffle = True)
        add_to_bin_f(x_trn, fn_trn_x, dtype = np.int32)
        add_to_bin_f(y_trn, fn_trn_y)
        add_to_bin_f(x_vl, fn_vl_x, dtype = np.int32)
        add_to_bin_f(y_vl, fn_vl_y)
        print('Обучающие и оценочные данные и их метки записаны в файлы\n',
              fn_trn_x, '\n', fn_trn_y, '\n', fn_vl_x, '\n', fn_vl_y)
#
elif do_tone:
    import time
    from keras.models import Model
    from keras.layers.embeddings import Embedding
    from keras.layers import Input, Flatten, Dense, Dropout
    if use_conv:
        from keras.layers.convolutional import Conv1D
        from keras.layers.pooling import GlobalMaxPooling1D
    #
    if not rand_weights:
        from gensim.models import Word2Vec
        def make_e_weights():
            print('Формируем массив с весами слоя Embedding')
            e_weights = np.zeros((num_words, size))
            for w, t in dict_w.items(): # Слово и его код
                w_coords = dict_w2v.get(w) # Координаты слова
                if w_coords is not None:
                    e_weights[t] = w_coords
##            print(e_weights[1])
            return e_weights
        print('Читаем файл', fn_wv)
        wv_model = Word2Vec.load(fn_wv)
        wv = wv_model.wv
        # wv.index2word - список слов словаря
        # wv.syn0 - массив координат слов
        dict_w2v = dict(zip(wv.index2word, wv.syn0))
##        print(list(dict_w2v.items())[0]) # Печать пары: слово-координаты_слова
##        print(dict_w2v.get('не')) # Печать координат слова 'не'
##        print(list(dict_w2v.keys())[0]) # Печать первого ключа
        e_weights = make_e_weights()
    #
    dict_w = make_dict_w()
    num_words = len(dict_w) + 1
    #
    print('Загрузка обучающего и оценочного множеств из файлов\n', fn_trn_x,
          '\n', fn_trn_y, '\n', fn_vl_x, '\n', fn_vl_y)
    x_trn, _ = read_bin_f(fn_trn_x, True, sq = max_len, dtype = np.int32)
    x_vl, _ = read_bin_f(fn_vl_x, True, sq = max_len, dtype = np.int32)
    y_trn = read_bin_f(fn_trn_y, False)
    y_vl = read_bin_f(fn_vl_y, False)
    #
    print('Формируем модель')
    inp = Input(shape = (max_len, ), dtype = 'int32')
    if rand_weights:
        from keras import initializers
        # TruncatedNormal RandomNormal he_normal glorot_uniform
        w_init = initializers.glorot_normal()
        x = Embedding(num_words, output_dim = size, input_length = max_len,
                      embeddings_initializer = w_init, trainable = False)(inp)
    else:
        # Слой по коду слова выдает координаты слова в e_weights (word2Vec-словаре)
        x = Embedding(num_words, output_dim = size, input_length = max_len,
                      weights = [e_weights], trainable = False)(inp)
        if predict_emb:
            import sys
            print('Проверка слоя Embedding')
            # Проверяем, действительно ли Embedding-слой по коду слова выдает
            # координаты слова в e_weights (word2Vec-словаре)
            def find_word_by_t(tf, p_coords):
                eps = 1.0e-7
                for w, t in dict_w.items():
                    if t == tf:
                        w_coords = dict_w2v.get(w)
                        eq = True
                        for m in range(len(p_coords)):
                            if abs(w_coords[m] - p_coords[m]) > eps:
                                eq = False
                                break
                        print('Число координат:', len(p_coords),
                              '\nКод слова:', tf, '\nСлово:', w, '\nРезультат:',
                              'w_coords = p_coords' if eq else 'w_coords <> p_coords')
                        return
                print('Координаты не найдены')
            model = Model(inp, x)
            model.summary()
            model.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy'])
            print('Прогноз')
            y_pred = model.predict(x_vl) # <class 'numpy.ndarray'>
            for k in range(2):
                find_word_by_t(x_vl[k][0], y_pred[k][0])
            sys.exit()
    if use_conv:
        x = Dropout(0.5)(x)
        x = Conv1D(filters = 3, kernel_size = 3, padding = 'same', activation = 'relu')(x)
    x = Flatten()(x)
    x = Dropout(0.3)(x)
    x = Dense(16, activation = 'relu')(x)
    output = Dense(1, activation = 'sigmoid')(x)
    model = Model(inp, output)
    model.summary()
    print('Обучаем модель')
    print('Используем', ('сжатые' if use_data_n else 'полные'), 'данные')
    model.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy'])
    start_time = time.time() # Время начала обучения
    model.fit(x_trn, y_trn, batch_size = batch, epochs = epochs,
              verbose = 2, validation_data = (x_vl, y_vl))
    # sg = 0; rand_weights = False; нет Conv1D; batch = 128; Dense(16):
    # val_acc: 0.6923 use_data_n = True; min_count = 3
    # val_acc: 0.6934 use_data_n = True; min_count = 5
    # val_acc: 0.6883 use_data_n = False; min_count = 5
    print('Время обучения:', time.time() - start_time)
#
elif do_token:
    import sys
    print('do_token: не используется'); sys.exit()
    # max_len - максимальное число слов в предложении
    from keras.preprocessing.sequence import pad_sequences
    if read_data_t:
        print('Читаем токенизированные данные из файла', fn_t)
        data_t0 = read_txt_f(fn_t)
        data_t = []
        p_max = 0
        p_min = 10
        for t in data_t0:
            seq = list(map(lambda x: int(x), t.split(' ')))
            p = max(seq)
            if p > p_max: p_max = p
            p = max(seq)
            if p < p_min: p_min = p
            data_t.append(seq)
        data_t = pad_sequences(data_t, maxlen = max_len, padding = 'post')
        print('p_min =', p_min, 'p_max =', p_max)
        print(data_t[0])
    else:
        # В результате токенизации каждое слово заменяется на уникальный номер - код слова
        # Если токенизированное предложение начинается с нулей,
        # то это означает, что число слов в предложении менее max_len
        # Если в функции pad_sequences задать padding = 'post',
        # то нули будут в конце предложения
        from keras.preprocessing.text import Tokenizer
        print('Токенизация данных. Читаем файл', fn_d_n)
        data_n = read_txt_f(fn_d_n) # encoding = None (для neg.txt и pos.txt)
        # Размер словаря - число строк в файле word_wv.txt
        word_wv = read_txt_f(fn_w)
        num_words = len(word_wv)
        #
        def get_sequences(tokenizer, x, do_padding = False, max_len = 30):
            sequences = tokenizer.texts_to_sequences(x) # <class 'list'>
            if do_padding:
                return pad_sequences(sequences, maxlen = max_len, padding = 'post')
            else:
                return sequences
        # Cоздаем и настраиваем токенизатор
        print('Настройка токенизатора')
        tokenizer = Tokenizer(num_words = num_words)
        tokenizer.fit_on_texts(data_n)
        # Отображаем каждый текст в массив идентификаторов токенов
        print('Выполняем токенизацию')
        data_t = get_sequences(tokenizer, data_n, do_padding = False, max_len = max_len)
        print(len(data_n), '\n', data_n[0], '\n', data_n[len(data_n) - 1])
        print(len(data_t), '\n', data_t[0], '\n', data_t[len(data_t) - 1])
        with open(fn_t, 'w') as f:
            for seq in data_t:
                s = ''
                for t in seq:
                    s = s + ' ' + str(t)
                s = s.lstrip()
                f.write((s + '\n') if s.find('\n') == -1 else s)
        print('Токенизированные данные записаны в файл', fn_t)
#
else:
    print('Все False')

Литература

  1. Рубцова Ю. В. Русскоязычный корпус коротких текстов. [Электронный ресурс] URL: http://study.mokoron.com/ (Дата обращения: 01.03.2020).
  2. Рубцова Ю. В. Построение корпуса текстов для настройки тонового классификатора // Программные продукты и системы, 2015, №1(109), – С.72-78.
  3. Морфологический словарь русского языка в виде SQL скрипта. [Электронный ресурс.] URL: https://shra.ru/2017/03/morfologicheskijj-slovar-russkogo-yazyka-v-vide-sql-skripta/ – (дата обращения: 01.03.2020).
  4. Efficient Estimation of Word Representations in Vector Space / T. Mikolov et al. // arXiv, 2013. [Электронный ресурс.] URL: http://arxiv.org/abs/1301.3781 – (дата обращения: 01.03.2020).
  5. Rubenstein H., Goodenough J. B. Contextual Correlates of Synonymy // Communications of the ACM, 1965, vol. 8, no. 10. – P. 627-633.
  6. Distributed Representations of Words and Phrases and their Compositionality / T. Mikolov et al. // arXiv, 2013. [Электронный ресурс.] URL: http://arxiv.org/abs/1310.4546.

Список работ

Рейтинг@Mail.ru