Visualizing Top Tweeps with t-SNE, in Javascript
Андрей Карпатый описывает однодневный проект по реализации алгоритма t-SNE на JavaScript и созданию интерактивной визуализации 500 самых популярных аккаунтов Twitter. Для сбора данных он спарсил список топ-аккаунтов с twitaholic.com через Kimono, затем скачал по 200 твитов каждого пользователя с помощью библиотеки Tweepy. Различия между аккаунтами вычислялись через TF-IDF-векторы (униграммы и биграммы) размерностью 87 342 с использованием scikit-learn. Полученная матрица расстояний передавалась в его библиотеку tsnejs для построения двумерной карты, на которой близко расположены пользователи со схожей тематикой твитов. Библиотека выложена на GitHub в открытом доступе, а в качестве бонуса автор также создал визуализацию векторных представлений слов.
I was recently looking into various ways of embedding unlabeled, high-dimensional data in 2 dimensions for visualization. A wide variety of methods have been proposed for this task. This Review paper from 2009 contains nice references to many of them (PCA, Kernel PCA, Isomap, LLE, Autoencoders, etc.). If you have Matlab available, the Dimensionality Reduction Toolbox has a nice implementation of many of these methods. Scikit Learn also has a brief section on Manifold Learning along with the implementation.
Недавно я изучал различные способы вложения немаркированных многомерных данных в двумерное пространство для визуализации. Для этой задачи было предложено множество методов. Этот обзорный доклад 2009 года содержит хорошие ссылки на многие из них (PCA, Kernel PCA, Isomap, LLE, автоэнкодеры и др.). Если у вас есть Matlab, в Dimensionality Reduction Toolbox реализовано множество этих методов. В Scikit Learn также есть краткий раздел по обучению на многообразиях с соответствующей реализацией.
Among these algorithms, t-SNE comes across as one that has a pleasing, intuitive formulation, simple gradient and nice properties. Here is a Google Tech Talks video of Laurens van der Maaten (the author) explaining the method. I set out to re-implement t-SNE from scratch since doing so is the best way of learning something that I know of, and what better language to do this in than - Javascript! :)
Среди этих алгоритмов t-SNE выделяется приятной, интуитивной формулировкой, простым градиентом и хорошими свойствами. Вот видео Google Tech Talks, где Laurens van der Maaten (автор метода) объясняет его суть. Я решил реализовать t-SNE с нуля, потому что это лучший известный мне способ чему-то научиться, — и на каком же языке это делать, как не на Javascript! :)
Long story short, I’ve implemented t-SNE in JS, released it as tsnejs on Github, and created a small demo that uses the library to visualize the top twitter accounts based on what they talk about. In this post, I thought it might be fun to document a small 1-day project like this, from beginning to end. This also gives me an opportunity to describe some of my projects toolkit, which others might find useful.
Короче говоря, я реализовал t-SNE на JS, выложил библиотеку как tsnejs на Github и создал небольшую демонстрацию, которая использует эту библиотеку для визуализации топовых аккаунтов Twitter по тематике их твитов. В этом посте я решил, что будет интересно задокументировать такой маленький однодневный проект от начала до конца. Заодно это даёт мне возможность описать некоторые инструменты из моего арсенала, которые могут пригодиться другим.
Final demo
Финальная демонстрация
First, take a look at the final demo. To create this demo I found the top 500 most followed accounts on Twitter, downloaded 200 of their tweets and then measured differences in what they tweet about. These differences are then fed to t-SNE to produce a 2-dimensional visualization, where nearby people tweet similar things. Fun!
Для начала взгляните на готовую демонстрацию. Для её создания я нашёл 500 самых популярных аккаунтов в Twitter, скачал по 200 их твитов и затем измерил различия в тематике их публикаций. Эти различия затем были переданы в t-SNE для построения двумерной визуализации, где близко расположенные люди пишут о похожих вещах. Забавно!
Fetching top tweeps
Получение списка топовых твиттерян
We first have to identify the top 500 tweeps. I googled “top twitter accounts” and found http://twitaholic.com/ , which lists them out. However, the accounts are embedded in the webpage and we need to extract them in structured format. For this, I love a recent YC startup Kimono; I use it extensively to scrape structured data from websites. It lets you click the elements of interest (the Twitter handles in this case), and extracts them out in JSON. Easy as pie!
Сначала нам нужно определить 500 самых популярных твиттерян. Я загуглил «top twitter accounts» и нашёл http://twitaholic.com/, где они перечислены. Однако аккаунты встроены в веб-страницу, и нам нужно извлечь их в структурированном формате. Для этого я обожаю недавний стартап из YC — Kimono; я постоянно использую его для извлечения структурированных данных с сайтов. Он позволяет кликнуть по нужным элементам (в данном случае — Twitter-хэндлам) и извлечь их в JSON. Проще простого!
Collecting tweets
Сбор твитов
Now we have a list of top 500 tweeps and we’d like to obtain their tweets to get an idea about what they tweet about. My library of choice for this task is Tweepy. Their documentation is quite terrible but if you browse the source code things seem relatively simple. Here’s an example call to get 200 tweets for a given user:
Теперь у нас есть список из 500 топовых твиттерян, и мы хотим получить их твиты, чтобы понять, о чём они пишут. Моя любимая библиотека для этой задачи — Tweepy. Документация у неё довольно ужасная, но если покопаться в исходном коде, всё оказывается относительно просто. Вот пример вызова для получения 200 твитов конкретного пользователя:
tweets = tweepy.Cursor(api.user_timeline, screen_name=user).items(200)
tweets = tweepy.Cursor(api.user_timeline, screen_name=user).items(200)
We iterate this over all users, extract the tweet text, and dumpt it all into files, one per account. I had to be careful with two annoyances in process:
Мы итерируем это по всем пользователям, извлекаем текст твитов и сохраняем всё в файлы — по одному на аккаунт. В процессе мне пришлось справиться с двумя неприятностями:
time.sleep calls.Twitter накладывает жёсткие лимиты на частоту API-вызовов, поэтому сбор данных занял несколько часов, обёрнутый в блоки try-catch и вызовы time.sleep. Возвращаемый текст в Unicode, что создаёт проблемы при попытке записи в файл.
One solution for the second annoyance is to use the codecs library:
Одно из решений второй проблемы — использовать библиотеку codecs:
import codecs codecs.open(filename, 'w', 'utf-8').write(tweet_texts)
import codecs codecs.open(filename, 'w', 'utf-8').write(tweet_texts)
Oh, and lets also grab and save the Twitter profile pictures, which we’ll use in the visualization. An example for one user might be:
Ах да, давайте также скачаем и сохраним аватарки профилей Twitter — они пригодятся нам в визуализации. Пример для одного пользователя:
import urllib # yes I know this is deprecated userobj = api.get_user(screen_name = user) urllib.urlretrieve(imgname, userobj.profile_image_url) # save image to disk
import urllib # да, я знаю, что это устарело userobj = api.get_user(screen_name = user) urllib.urlretrieve(imgname, userobj.profile_image_url) # сохраняем изображение на диск
I should mention that I write a lot of quick and dirty Python code in IPython Notebooks, which I very warmly recommend. If you’re writing all your Python in text editors, you’re seriously missing out.
Должен упомянуть, что я пишу много быстрого чернового кода на Python в IPython Notebooks, которые я горячо рекомендую. Если вы пишете весь свой Python в текстовых редакторах — вы серьёзно многое упускаете.
Quantifying Tweep differences
Количественная оценка различий между твиттерянами
We now have 500 tweeps and their 200 most recent tweets concatenated in 500 files. We’d now like to find who tweets about similar things. Scikit learn is very nice for quick NLP tasks like this. In particular, we load up all the files and create a 500-long array where every element are the 200 concatenated tweets. Then we use the TfidfVectorizer class to extract all words and bigrams from the text data, and to turn every user’s language into one tfidf vector. This vector is a fingerprint of the language that each person uses. Here’s how we can simply wire this up:
Теперь у нас есть 500 твиттерян и их 200 последних твитов, объединённых в 500 файлов. Теперь мы хотим найти тех, кто пишет о похожих вещах. Scikit learn отлично подходит для быстрых NLP-задач вроде этой. В частности, мы загружаем все файлы и создаём массив из 500 элементов, где каждый элемент — 200 объединённых твитов. Затем мы используем класс TfidfVectorizer для извлечения всех слов и биграмм из текстовых данных и преобразования языка каждого пользователя в один tfidf-вектор. Этот вектор — своего рода отпечаток языка, который использует каждый человек. Вот как это можно просто настроить:
from sklearn.feature_extraction.text import CountVectorizer,TfidfVectorizer vectorizer = TfidfVectorizer(min_df=2, stop_words = 'english',\ strip_accents = 'unicode', lowercase=True, ngram_range=(1,2),\ norm='l2', smooth_idf=True, sublinear_tf=False, use_idf=True) X = vectorizer.fit_transform(user_language_array) D = -(X * X.T).todense() # Distance matrix: dot product between tfidf vectors
from sklearn.feature_extraction.text import CountVectorizer,TfidfVectorizer vectorizer = TfidfVectorizer(min_df=2, stop_words = 'english',\ strip_accents = 'unicode', lowercase=True, ngram_range=(1,2),\ norm='l2', smooth_idf=True, sublinear_tf=False, use_idf=True) X = vectorizer.fit_transform(user_language_array) D = -(X * X.T).todense() # Матрица расстояний: скалярное произведение tfidf-векторов
In the above, user_language_array is the 500-element array that has the concatenated tweets. The TfidfVectorizer class looks through all tweets and takes note of all words (unigrams) and word bigrams (i.e. series of two words). It builds a dictionary out of all unigram/bigrams and essentially counts up how often every person uses each one. Here’s an example of some tweet text converted to unigram/bigrams:
В коде выше user_language_array — это массив из 500 элементов с объединёнными твитами. Класс TfidfVectorizer просматривает все твиты и фиксирует все слова (униграммы) и словесные биграммы (то есть последовательности из двух слов). Он строит словарь из всех униграмм/биграмм и по сути подсчитывает, как часто каждый человек использует каждую из них. Вот пример преобразования текста твита в униграммы/биграммы:
The tfidf vectors are returned stacked up as rows inside X, which has size 500 x 87,342. Every one of the 87,342 dimensions corresponds to some unigram or bigram. For example, the 10,000th dimension could correspond to the frequency of usage of the unigram “YOLO”. The vectors are L2 normalized, so the dot product between these vectors is related to the angle between any two vectors. This can be interpreted as the similarity of language. Finally, we dump the matrix and the usernames into a JSON file, and we’re ready to load things up in Javascript!
Tfidf-векторы возвращаются сложенными в строки матрицы X размером 500 x 87 342. Каждое из 87 342 измерений соответствует некоторой униграмме или биграмме. Например, 10 000-е измерение может соответствовать частоте использования униграммы «YOLO». Векторы нормализованы по L2, поэтому скалярное произведение между ними связано с углом между любыми двумя векторами. Это можно интерпретировать как сходство языка. Наконец, мы сохраняем матрицу и имена пользователей в JSON-файл — и мы готовы загрузить всё в Javascript!
The Visualization parts
Визуализация
We now create an .html file and import jQuery (as always), and d3js, which I like to use for any kind of plotting. We load up the JSON that stores our distances and usernames with jQuery, and use d3js to initialize the SVG element that will hold all the users. For starters, we plot the users at random position but we will soon arrange them so that similar users cluster nearby with t-SNE. Inspect the code on the demo page to see the jQuery and d3js parts (Ctrl+U). In the code, we see a few things I like to use:
Теперь создаём .html-файл и подключаем jQuery (как всегда) и d3js, который я люблю использовать для любых графиков. Загружаем JSON с расстояниями и именами пользователей через jQuery, а с помощью d3js инициализируем SVG-элемент, который будет содержать всех пользователей. Для начала мы размещаем пользователей в случайных позициях, но вскоре расположим их так, чтобы похожие пользователи оказались рядом — с помощью t-SNE. Изучите код на странице демо, чтобы увидеть части с jQuery и d3js (Ctrl+U). В коде мы видим несколько вещей, которые я люблю использовать:
Мне нравится использовать Google Fonts для получения шрифтов красивее стандартных. Здесь, например, я подключаю Roboto и затем использую его в CSS. Далее мы видим подключение кода syntaxhighlighter, который динамически подсвечивает код на странице. Затем идёт JS-код отслеживания Google, который позволяет мне отслеживать статистику сайта в Google Analytics. На этом сайте я не использовал Bootstrap, потому что он очень маленький и простой, но обычно использую — это сразу делает сайт нормально работающим на мобильных устройствах.
t-SNE
t-SNE
Finally we get to the meat! We need to arrange the users in our d3js plot so that similar users appear nearby. The t-SNE cost function was described in this 2008 paper by van der Maaten and Hinton. Similar to many other methods, we set up two distance metrics in the original and the embedded space and minimize their difference. In t-SNE in particular, the original space distance is based on a Gaussian distribution and the embedded space is based on the heavy-tailed Student-t distribution. The KL-divergence formulation has the nice property that it is asymmetric in how it penalizes distances between the two spaces:
Наконец добираемся до сути! Нам нужно расположить пользователей на нашем графике d3js так, чтобы похожие пользователи оказались рядом. Функция стоимости t-SNE была описана в статье 2008 года van der Maaten и Hinton. Как и во многих других методах, мы задаём две метрики расстояния — в исходном и во вложенном пространствах — и минимизируем их различие. В t-SNE, в частности, расстояние в исходном пространстве основано на распределении Гаусса, а во вложенном — на тяжелохвостом распределении Стьюдента. Формулировка через KL-дивергенцию обладает приятным свойством асимметричности в штрафовании расстояний между двумя пространствами:
Если две точки близки в исходном пространстве, между ними действует сильная притягивающая сила во вложении. И наоборот, если две точки далеки в исходном пространстве, алгоритм относительно свободен в их размещении.
Thus, the algorithm preferentially cares about preserving the local structure of the high-dimensional data. Conveniently, the authors link to multiple implementations of t-SNE on their website, which allows us to see some code for reference as well (if you’re like me, reading code can be much easier than reading text descriptions). We’re ready to write up the Javascript version!
Таким образом, алгоритм в первую очередь заботится о сохранении локальной структуры многомерных данных. К удобству, авторы размещают ссылки на множество реализаций t-SNE на своём сайте, что позволяет нам также посмотреть код для справки (если вы похожи на меня, читать код бывает гораздо легче, чем текстовые описания). Мы готовы писать версию на Javascript!
The final code can be seen in tsne.js file, on Github. Note how we’re wrapping all the JS code into a function closure so that we don’t pollute the global namespace. This is a very common trick in Javascript that is essentially used to implement classes. Note also the large number of utility boring code I had to include up top because Javascript is not exactly intended for math :) The core function where all magic happens is costGrad(), which computes the cost function and the gradient of the objective. The correct implementation of this function is double checked with debugGrad() gradient check. Once the analytic gradient checks out compared to numeric gradient, we’re good to go! We set up a piece of Javascript to call our step() function repeatedly (setInterval() call), and we plot the solution as it gets computed.
Финальный код можно увидеть в файле tsne.js на Github. Обратите внимание, как мы оборачиваем весь JS-код в замыкание, чтобы не засорять глобальное пространство имён. Это очень распространённый приём в Javascript, который по сути используется для реализации классов. Обратите также внимание на большой объём скучного служебного кода в начале, потому что Javascript не совсем предназначен для математики :) Основная функция, где происходит вся магия, — это costGrad(), которая вычисляет функцию стоимости и градиент целевой функции. Корректность реализации этой функции проверяется численной проверкой градиента в debugGrad(). Как только аналитический градиент совпадает с численным — можно запускать! Мы настраиваем фрагмент Javascript для многократного вызова функции step() (вызов setInterval()) и отображаем решение по мере его вычисления.
Phew! Final result, again: t-SNE demo.
Уф! Финальный результат, ещё раз: демонстрация t-SNE.
I hope some of the references were useful. If you use tsnejs to embed some of your data, let me know!
Надеюсь, некоторые из упомянутых ссылок оказались полезными. Если вы используете tsnejs для вложения своих данных — дайте мне знать!
Bonus: Word Embedding t-SNE Visualization
Бонус: визуализация векторных представлений слов с помощью t-SNE
I created another demo, this time to visualize word vector embeddings. Head over here to see it. The word embeddings are trained as described in this ACL 2012 paper.
Я создал ещё одну демонстрацию — на этот раз для визуализации векторных представлений слов. Перейдите сюда, чтобы посмотреть её. Векторные представления слов обучены по методу, описанному в статье ACL 2012.
The (unsupervised) objective function makes it so that words that are interchangable (i.e. occur in very similar surrounding context) are close in the embedding. This comes across in the visualization!
Целевая функция (без учителя) устроена так, что слова, которые взаимозаменяемы (то есть встречаются в очень похожем окружающем контексте), оказываются близки во вложении. Это хорошо видно в визуализации!