Рассматривается задача определения тональности высказываний пользователей интернета средствами нейронной сети (НС) с использованием 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-представления слов, обучающего и оценочного множеств и создание НС выполняется в следующем порядке:
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-модели создается по загруженному на [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.
Выполняется следующим кодом:
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-модели из слов корпуса формируются словарь и вектор с координатами каждого слова в линейном пространстве, размерность которого определяется параметром 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 слов (как-бы слов).
Выполняется следующим кодом:
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.
Выполняется следующим кодом:
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.
Выполняется следующим кодом:
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.
Выполняется следующим кодом:
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.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, на входе которого закодированное предложение (см. пред. разд.), а на выходе – массив с координатами слов предложения в сформированной ранее 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. То есть большая часть слов корпуса оказалась исключенной из процесса классификации. Во-вторых, бинарная классификация высказываний является неполной, поскольку, как уже отмечалось выше, высказывание может быть не только позитивным или негативным, но и иметь иной оттенок, например, нейтральный. Пути преодоления этих обстоятельств очевидны: автоматическое исправление ошибок, не меняющее интонацию высказывания, и увеличение числа классов. В то же время можно попытаться улучшить ситуацию, предприняв следующие действия:
Прежде указываем используемые библиотеки:
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 = wv.syn0
# или
vec_list = wv.vectors
#
# Вывод трех случайно выбранных слов и их координат
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)
#
# Прогноз центрального слова, заданного списком слов контекста
output_word = wv_model.predict_output_word(['испугалась', 'когда', 'он', 'пришел', 'ко', 'мне'], topn = 5)
print(output_word)
Больше информации см. в других источниках.
Word2vec – это один из инструментов распределенного представления слов [4]. Идея word2vec состоит в том, чтобы научиться предсказывать слово из его контекста (модель непрерывного мешка CBOW) или наоборот – контекст по слову (модель Skip-gram).
Word2vec-модель строится исходя из дистрибутивной гипотезы [5], предполагающей, что слова с похожим смыслом встречаются похожих контекстах. В модели каждое слово представляется вещественным вектором (word embedding) заданной длины. При формировании модели в результате анализа окружения слова (контекста) близкие по значению слова (с точки зрения модели) представляются векторами, расстояние между которыми незначительно, и наоборот, слова, существующие в несовпадающих контекстах, представляются векторами, расстояние между которыми весьма велико.
Иными словами, выполняется максимизация близости векторов слов, которые появляются рядом друг с другом, и минимизация близости векторов слов, которые не появляются друг рядом с другом. Оценку расстояния между векторами можно выполнять, например, используя косинусную близость.
Суть модели CBOW заключается в том, чтобы научиться как можно лучше решать следующую задачу: по заданному контексту слова восстановить само слово. Предсказывается слово по левому и правому контекстам (рис. 1).
Модель Skip-gram работает противоположным образом: пытается предсказать каждое слово контекста по данному слову (рис. 1).
Рис. 1. Модели CBOW и Skip-gram
В обеих моделях размер окна контекста равен 2 (window = 2). Число слов в контексте будет меньше, если, например, обрабатывается первое или последнее слово документа (в качестве документа может выступать, например, абзац текста или отдельное предложение).
Создание word2Vec-модели может состоять из следующих этапов:
В случае CBOW обучающее множество совпадает с C, а множество меток – с M. В случае Skip-gram все ровно наоборот.
Предсказание слова по контексту или контекста по слову выполняется нейронной сетью с (НС) одним скрытым слоем (рис. 2), имеющим линейную функцию активации – это означает, что при вычислении выхода нейрона скрытого слоя усредняются поступившие на нейрон сигналы.
Рис. 2. Сруктуры нейронных сетей для получения CBOW- и Skip-gram-моделей
В обеих НС число нейронов на скрытом слое равно size – размеру вектора, представляющего слово (размерность пространства word2Vec-модели). Длина массива W равна nV. Выход скрытого слоя – это массив W' формы (nV, size). После завершения процесса обучения нейронной сети i-я строка массива W' рассматривается как вектор, представляющий слово wi словаря V.
В случае CBOW перебираются массивы множества C, и выбранный массив Ci – контекст слова wi подается на вход НС. Таким образом, на вход НС поступает h * nV сигналов xi ∈ Ci.
Выходной слой НС с функцией активации softmax содержит nV нейронов. При вычислении ошибки берутся прогноз НС – вектор y_pred и вектор y_true, имеющий, если 1 в позиции i и нули во всех прочих позициях. Для вычисления потерь можно использовать перекрестную энтропию.
В качестве целевой функции используется общее правдоподобие корпуса текста. Значение правдоподобия максимизируется. Целевая функция записывается следующим образом:
,где θ – параметры модели;
w – слово;
c – локальный контекст слова;
V – словарь;
C – множество локальных контекстов.
В случае Skip-gram имеем следующую целевую функцию:
Вероятность моделируется функцией softmax.
Для уменьшения вычислительной сложности в обеих моделях используется либо иерархический softmax, либо отрицательное сэмплирование, а для повышения точности часто употребляемые слова изымаются в Skip-gram модели из обучающего множества с вероятностью, зависящей от частоты присутствия слова в корпусе текста [6].
Word2Vec-модель пытается отразить семантические взаимосвязи слов в выбранном корпусе текста, и при наличии соответствующего корпуса делает это весьма успешно. В то же время она имеет ряд недостатков, например, следующие:
Омонимы – это слова, совпадающее с по написанию, но разные по значению.
Примеры.
Некоторые омонимы слова коса:
Некоторые омонимы слова косой:
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')