У нас есть слова и есть компьютер, который должен с этими словами как-то работать. Вопрос — как компьютер будет работать со словами? Идеи:
Суть — закодировать слова цифрами по порядку следования в словаре.
Но у этой идеи есть и существенный недостаток: слова в словаре следуют в алфавитном порядке, и при добавлении слова нужно перенумеровывать заново большую часть слов. Но даже это не является настолько важным, а важно то, буквенное написание слова никак не связано с его смыслом (эту гипотезу еще в конце XIX века высказал известный лингвист Фердинанд де Соссюр). В самом деле слова “петух”, “курица” и “цыпленок” имеют очень мало общего между собой и стоят в словаре далеко друг от друга, хотя очевидно обозначают самца, самку и детеныша одного вида птицы. То есть мы можем выделить два вида близости слов: лексический и семантический. Как мы видим на примере с курицей, эти близости не обязательно совпадают. Можно для наглядности привести обратный пример лексически близких, но семантически далеких слов — "зола" и "золото".
Чтобы получить возможность представить семантическую близость, было предложено использовать embedding, то есть сопоставить слову некий вектор, отображающий его значение в “пространстве смыслов”. Embedding — это сопоставление произвольной сущности (например, узла в графе или кусочка картинки) некоторому вектору. Векторное представление даёт возможность сравнивать тексты, сравнивая представляющие их вектора в какой-либо метрике (евклидово расстояние, косинусная мера, манхэттенское расстояние, расстояние Чебышёва и др.), то есть производя кластерный анализ.
Какой самый простой способ получить вектор из слова? Кажется, что естественно будет взять вектор длины нашего словаря и поставить только одну единицу в позиции, соответствующей номеру слова в словаре. Этот подход называется one-hot encoding (OHE). OHE все еще не обладает свойствами семантической близости.
Значение одного слова нам может быть и не так важно, т.к. речь (и устная, и письменная) состоит из наборов слов, которые мы называем текстами. Так что если мы захотим как-то представить тексты, то мы возьмем OHE-вектор каждого слова в тексте и сложим вместе. Т.е. на выходе получим просто подсчет количества различных слов в тексте в одном векторе. Такой подход называется “мешок слов” (bag of words, BoW), потому что мы теряем всю информацию о взаимном расположении слов внутри текста.
Мы можем пойти дальше и представить наш корпус (набор текстов) в виде матрицы “слово-документ” (term-document). Эта матрица приводит нас к тематическим моделям, где матрицу “слово-документ” пытаются представить в виде произведения двух матриц “слово-тема” и “тема-документ”. В самом простом случае мы возьмем матрицу и с помощью SVD-разложения получим представление слов через темы и документов через темы.
Эта аббревиатура означает "term frequency — inverse document frequency", что является модификацией матрицы term-document. Эта мера используется для оценки важности слова в контексте документа, являющегося частью коллекции документов или корпуса. Вес некоторого слова пропорционален количеству употребления этого слова в документе, и обратно пропорционален частоте употребления слова в других документах коллекции.
TF (term frequency — частота слова) — отношение числа вхождений некоторого слова к общему числу слов документа. Таким образом, оценивается важность слова в пределах отдельного документа.
где есть число вхождений слова в документ, а в знаменателе — общее число слов в данном документе.
IDF (inverse document frequency — обратная частота документа) — инверсия частоты, с которой некоторое слово встречается в документах коллекции. Учёт IDF уменьшает вес широкоупотребительных слов. Для каждого уникального слова в пределах конкретной коллекции документов существует только одно значение IDF.
где
Таким образом, мера TF-IDF является произведением двух сомножителей:
Большой вес в TF-IDF получат слова с высокой частотой в пределах конкретного документа и с низкой частотой употреблений в других документах.
Описанные выше подходы были (и остаются) хороши для времен (или областей), где количество текстов мало и словарь ограничен, хотя, как мы видели, там тоже есть свои сложности. Но с приходом в нашу жизнь интернета все стало одновременно и сложнее и проще: в доступе появилось великое множество текстов, и эти тексты с изменяющимся и расширяющимся словарем. С этим надо было что-то делать, а ранее известные модели не могли справиться с таким объемом текстов. Количество слов в английском языке очень грубо составляет миллион — матрица совместных встречаемостей только пар слов будет . Такая матрица даже сейчас не очень лезет в память компьютеров, а, скажем, 10 лет назад про такое можно было не мечтать.
И тогда, как это часто бывает, был предложен выход по принципу “тот, кто нам мешает, тот нам поможет!” А именно, в 2013 году тогда мало кому известный чешский аспирант Томаш Миколов предложил свой подход к word embedding, который он назвал word2vec. Его подход основан на другой важной гипотезе, которую в науке принято называть гипотезой локальности — “слова, которые встречаются в одинаковых окружениях, имеют близкие значения”. Близость в данном случае понимается очень широко, как то, что рядом могут стоять только сочетающиеся слова. Например, для нас привычно словосочетание "заводной будильник". А сказать “заводной апельсин” мы не можем* — эти слова не сочетаются.
Основываясь на этой гипотезе Томаш Миколов предложил новый подход, который не страдал от больших объемов информации, а наоборот выигрывал. Модель, предложенная Миколовым очень проста (и потому так хороша) — мы будем предсказывать вероятность слова по его окружению (контексту). То есть мы будем учить такие вектора слов, чтобы вероятность, присваиваемая моделью слову была близка к вероятности встретить это слово в этом окружении в реальном тексте.
Здесь — вектор целевого слова, — это некоторый вектор контекста, вычисленный (например, путем усреднения) из векторов окружающих нужное слово других слов. А — это функция, которая двум векторам сопоставляет одно число.
Процесс тренировки устроен следующим образом: мы берем последовательно (2k+1) слов, слово в центре является тем словом, которое должно быть предсказано. А окружающие слова являются контекстом длины по k с каждой стороны. Каждому слову в нашей модели сопоставлен уникальный вектор, который мы меняем в процессе обучения нашей модели.
В целом, этот подход называется CBOW — continuous bag of words, continuous потому, что мы скармливаем нашей модели последовательно наборы слов из текста, a BoW потому что порядок слов в контексте не важен.
Также Миколовым сразу был предложен другой подход — прямо противоположный CBOW, который он назвал skip-gram, то есть “словосочетание с пропуском”. Мы пытаемся из данного нам слова угадать его контекст (точнее вектор контекста). В остальном модель не претерпевает изменений.
Что стоит отметить: хотя в модель не заложено явно никакой семантики, а только статистические свойства корпусов текстов, оказывается, что натренированная модель word2vec может улавливать некоторые семантические свойства слов. Например:
Слово "мужчина" относится к слову "женщина" так же, как слово "дядя" к слову "тётя", что для нас совершенно естественно и понятно, но в других моделям добиться такого же соотношения векторов можно только с помощью специальных ухищрений. Здесь же — это происходит естественно из самого корпуса текстов. Кстати, помимо семантических связей, улавливаются и синтаксические, справа показано соотношение единственного и множественного числа.
Помимо word2vec были, само собой, предложены и другие модели word embedding. Стоит отметить модель, предложенную лабораторией компьютерной лингвистики Стенфордского университета, под названием Global Vectors (GloVe), сочетающую в себе черты SVD разложения и word2vec, а так же fasttext от facebook.
import string import numpy as np import pandas as pd import matplotlib as mpl import matplotlib.pyplot as plt import seaborn as sns from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from nltk.stem import SnowballStemmer from nltk.corpus import stopwords import re %matplotlib inline
from gensim.models import KeyedVectors as kv
w2v = kv.load_word2vec_format("./all.norm-sz100-w10-cb0-it1-min100.w2v", binary=True, unicode_errors='ignore')
w2v["дерево"]
w2v.init_sims(replace=True) for word, score in w2v.most_similar("дерево"): print(word, score)
w2v.most_similar(positive=["женщина", "король"], negative=["мужчина"])
w2v.doesnt_match("собака цыплёнок курица петух несушка".split())
w2v.doesnt_match("белый красный черный тяжелый".split())
w2v.similarity("москва", "париж"), w2v.similarity("женщина", "девочка"), w2v.similarity("женщина", "мужчина")
from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.naive_bayes import MultinomialNB from sklearn.tree import DecisionTreeClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.ensemble import RandomForestClassifier from sklearn.ensemble import AdaBoostClassifier from sklearn.ensemble import BaggingClassifier from sklearn.ensemble import ExtraTreesClassifier from sklearn.metrics import accuracy_score
svc = SVC(kernel='sigmoid', gamma=1.0) knc = KNeighborsClassifier(n_neighbors=49) mnb = MultinomialNB(alpha=0.2) dtc = DecisionTreeClassifier(min_samples_split=7, random_state=111) lrc = LogisticRegression(solver='liblinear', penalty='l1') rfc = RandomForestClassifier(n_estimators=31, random_state=111) abc = AdaBoostClassifier(n_estimators=62, random_state=111) bc = BaggingClassifier(n_estimators=9, random_state=111) etc = ExtraTreesClassifier(n_estimators=9, random_state=111)
Попробуем классифицировать тексты, размеченные на http://linis-crowd.org/.
polit = pd.read_excel("collection (docs&words)_2016_all_labels/doc_comment_summary.xlsx", header=None)
polit.head()
0 | 1 | |
---|---|---|
0 | Но при мужчине ни одна приличная женщина не по... | -1 |
1 | Украина это часть Руси искусственно отделенная... | -1 |
2 | Как можно говорить об относительно небольшой к... | -1 |
3 | 1.2014. а что они со своими поляками сделали?... | 0 |
4 | у а фильмы... Зрители любят диковинное. у ме... | 0 |
polit.columns = ["text", "score"]
polit["score"].value_counts()
(~polit["score"].isin(range(-2, 3))).sum()
polit = polit[polit["score"].isin(range(-2, 3))]
polit["score"] = pd.to_numeric(polit["score"])
polit.hist(column="score")
def trinarize(score): if score < 0: return 2 if score > 0: return 1 else: return 0 polit["new_score"] = polit["score"].apply(trinarize)
polit["new_score"].value_counts(normalize=True).plot(kind="bar")
from segtok.tokenizer import symbol_tokenizer, word_tokenizer, web_tokenizer
from pymystem3 import Mystem m = Mystem()
polit["mystem"] = polit["text"].apply(str).apply(m.lemmatize) #polit["text"].apply(str).apply(str.lower).apply(lambda text: re.sub("[^\w\s]", " ", text)).apply(word_tokenizer)
def remove_white_and_punct(tokens): tokens = filter(lambda token: not bool(re.match("^[\s\W]+$", token)), tokens) return list(tokens)
list(map(remove_white_and_punct, [["fdfs", " ", " ", "?!"], ["fdfs", " ", " "]]))
polit["preprocessed"] = polit["mystem"].apply(remove_white_and_punct)
polit["preprocessed"]
from nltk.corpus import stopwords nltk.download("stopwords")
from collections import Counter
from collections import Counter c = Counter() for _id, doc in polit["preprocessed"].iteritems(): c.update(doc)
len(c)
c.most_common(30)
most_freq = [w for w, c in c.most_common(30)] most_rare = [w for w, c in c.items() if c < 6]
len(most_rare)
most_rare[:10]
most_rare
stopw = set(most_freq + most_rare + stopwords.words("russian"))
len(stopw)
polit["preprocessed"] = polit["preprocessed"].apply( lambda tokens: " ".join([token for token in tokens if token not in stopw]))
polit["preprocessed"].head()
from gensim.models.phrases import Phrases, Phraser
phrases = Phrases((doc.split() for _id, doc in polit["preprocessed"].iteritems()))
bigram = Phraser(phrases)
polit["preprocessed"] = polit["preprocessed"].apply(lambda x: bigram[x.split()])
polit["preprocessed"].head()
from sklearn.feature_extraction.text import CountVectorizer
count_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(2, 2)) count_features = count_vectorizer.fit_transform(polit["preprocessed"])
count_features
count_vectorizer.get_feature_names()
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vectorizer = TfidfVectorizer() tfidf_features = tfidf_vectorizer.fit_transform([" ".join(doc) for _id, doc in polit["preprocessed"].iteritems()])
tfidf_features
features_train, features_test, labels_train, labels_test = train_test_split( tfidf_features, polit["new_score"], test_size=0.3, random_state=42)
from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report
mnb = MultinomialNB(alpha=0.2)
mnb.fit(features_train, labels_train)
predicted = mnb.predict(features_test)
predicted
print(classification_report(labels_test, predicted))
Постройте классификатор, который отделяет спам от не-спама используя следующий набор данных: SMS Spam Collection Dataset.
sms = pd.read_csv("spam.csv", encoding="latin-1") sms.head()
v1 | v2 | Unnamed: 2 | Unnamed: 3 | Unnamed: 4 | |
---|---|---|---|---|---|
0 | ham | Go until jurong point, crazy.. Available only ... | NaN | NaN | NaN |
1 | ham | Ok lar... Joking wif u oni... | NaN | NaN | NaN |
2 | spam | Free entry in 2 a wkly comp to win FA Cup fina... | NaN | NaN | NaN |
3 | ham | U dun say so early hor... U c already then say... | NaN | NaN | NaN |
4 | ham | Nah I don't think he goes to usf, he lives aro... | NaN | NaN | NaN |
sms = sms.drop(['Unnamed: 2','Unnamed: 3','Unnamed: 4'],axis=1) sms = sms.rename(columns = {'v1':'label','v2':'message'})
sms.groupby('label').describe()
message | ||||
---|---|---|---|---|
count | unique | top | freq | |
label | ||||
ham | 4825 | 4516 | Sorry, I'll call later | 30 |
spam | 747 | 653 | Please call our customer service representativ... | 4 |
sms['length'] = sms['message'].apply(len) sms.head()
label | message | length | |
---|---|---|---|
0 | ham | Go until jurong point, crazy.. Available only ... | 111 |
1 | ham | Ok lar... Joking wif u oni... | 29 |
2 | spam | Free entry in 2 a wkly comp to win FA Cup fina... | 155 |
3 | ham | U dun say so early hor... U c already then say... | 49 |
4 | ham | Nah I don't think he goes to usf, he lives aro... | 61 |
sms.hist(column='length', by='label', bins=50, figsize=(11, 5));
def text_process(text): text = text.translate(str.maketrans('', '', string.punctuation)) text = [word for word in text.split() if word.lower() not in stopwords.words('english')] return " ".join(text)
Комментарии