Trawling Twitter for Trollish Tweets
By: Byron Allen
Tags: Disinformation, Machine Learning, Scikit Learn, Twitter, NLP

Disinformation is one of the greatest threats to democracy. Fake news, trolling, and other politically and socially subversive content is plaguing society. Social media giants have played a role in enabling and incubating this behaviour. Several have even appeared before the U.S. Congress and the EU Parliament, promising to do more to address the challenge of disinformation and hate speech on social media. Some of these promises have taken nearly a year and are still limited.

So, when the opportunity came to create a demo of a production machine learning (ML) system in Google Cloud Platform (GCP), I jumped at the chance to create a trolling tweet detector. The demo called for a full project structure on GCP that would generate and deploy a custom ML estimator to Cloud ML Engine.

I could have chosen something more "businessie". However, the use case of disinformation identification is motivating to me. The harm of disinformation and hate speech is real, and I wanted to put my foot in the ring on this topic, albeit a humble baby step.

In this post, I will focus on the dataset and model development specific to this trollish tweet detector, and in a follow-up article, I will discuss tracking model performance, using MLFlow, and deploying a model using GCP. Previously, I wrote about a ProductionML Framework, which covers this topic at a broader, architectural level and calls to bridge the gap between data scientists and data engineers.

Training and Testing on a Trolling Dataset

Collecting Data

The dataset I used is a collection of tweets shared by FiveThirtyEight in their storyWhy We're Sharing 3 Million Russian Troll Tweets.

This is what FiveThirtyEight has to say:

FiveThirtyEight obtained the data from Clemson University researchers Darren Linvill, an associate professor of communication, and Patrick Warren, an associate professor of economics, on July 25, 2018. They gathered the data using custom searches on a tool called Social Studio, owned by Salesforce and contracted for use by Clemson's Social Media Listening Center... ...[t]his directory contains data on nearly 3 million tweets sent from Twitter handles connected to the Internet Research Agency, a Russian 'troll factory' and a defendant in an indictment filed by the Justice Department in February 2018, as part of special counsel Robert Mueller's Russia investigation. The tweets in this database were sent between February 2012 and May 2018, with the vast majority posted from 2015 through 2017.

The dataset is a series of CSV files stored in a GitHub repository. I downloaded them to my local machine and used Python's Pandas library to union each dataframe together. At that point, I had the complete dataset in a unified structure, which I then stored in BigQuery, again using Pandas.

import os

import pandas as pd # Union

paths = [os.path.join("../data", x) for x in os.listdir("../data")]

cols = pd.read_csv(paths[0]).columns

df = pd.DataFrame(columns=cols) # roughly 3+ million records for path in paths:

df_tmp = pd.read_csv(path)

df = pd.concat([df, df_tmp]) # Store in BigQuery

df.to_gbq(

destination_table="dataset.table_name"

, project_id=project_id

, chunksize=None

, verbose=None

, reauth=False

, if_exists='fail'

, private_key=None

, auth_local_webserver=False

, table_schema=table_schema

)

The key features I used further downstream were account_category and content fields. However, it is always good practice to land your full dataset and then, through BigQuery views, transform as needed to ensure you can leverage more features in the future if needed. Views, if you don't know, do not create a new table - nothing new is stored, no new storage costs are incurred. Simply, a view is the query saved until it is needed.

Class Imbalance

After reading the documentation on the dataset and doing an initial exploratory data analysis (EDA), I came to the conclusion that there are some challenges that need to be addressed related to class imbalance.

Firstly, there are eight classes, or categories, under account_category . All come from handles suspected or confirmed to be related to the Internet Research Agency (IRA), so none are non-trolls, rather they are all various types of trolls related to IRA.

Here are the classes along with some samples of the content associated with each class:

RightTroll

“We have a sitting Democrat US Senator on trial for corruption and you’ve barely heard a peep from the mainstream media.” ~ @nedryun

Marshawn Lynch arrives to game in anti-Trump shirt. Judging by his sagging pants the shirt should say Lynch vs. belt

Daughter of fallen Navy Sailor delivers powerful monologue on anthem protests, burns her NFL packers gear. #BoycottNFL

JUST IN: President Trump dedicates Presidents Cup golf tournament trophy to the people of Florida, Texas and Puerto Rico.

19,000 RESPECTING our National Anthem! #StandForOurAnthem🇺🇸

NonEnglish

Причина #67 Мутко: «Зенит-Арене» для адаптации к ЧМ потребуется 1,5–2 млрд рублей

Причина #70 Житель Самары умер в очереди в поликлинике

Причина #74 Президентский советник предложил ограничить Интернет, как в Китае

Причина #75 Казаков благословили на защиту интернет-пространства

Причина #77 В Кстово полицейские сломали женщине лицевую кость, когда та отказалась показать грудь

Fearmonger

#FoodPoisoning is not a joke! #Walmart #KochFarms

Thanks God that #turkey i ate few days ago was good. #KochFarms

I hurt easy, I just don’t show it. You can hurt someone and not even know it.

Kim and kanye wedding

who’s got the worst make up ever, don’t tell me, mirror

LeftTroll

#blacklivesmatter #blm #equality #equalrights

‘@big_gun_in_utah so? If you pay more taxes you have more rights? This how it works?’

‘@big_gun_in_utah some of you?’

Black Students force the university to divest $25 Million in private prisons #blacktwitter

Racist Maine Gov. on ‘drug problem’ #RacistScum #idiot

Unknown

MY DENTIST ASKED HIS ASSISTANT TO SUCTION (THE WATER OUT OF MY MOUTH)BUT I THOUGHT HE WAS TALKING TO ME SO I SUCKED HIS FINGER. IM MORTIFIED

48. “I don’t eat buffalo”

the greatest line in journalism history

Never seen so many facial expressions in a short amount of time

i can’t believe that cows are the cutest n most perfect animals in this whole entire universe :-)

HashtagGamer

This week’s @GooglyTags hosted by @googlygirl98 is #ItWasJustThatOneTime

The time spent by senators of the president’s own party asking about situations where POTUS might misbehave seems abnormally high.

#TodayIsGoingToBeGreatBecause I choose to not give a fuck what anyone says or does!! It’s my life and I live for me.

In all seriousness, if Sessions goes after porno Trump is gonna lose the Anime Nazis once and for all

#TodayIsGoingToBeGreatBecause I have wine

NewsFeed

Albuquerque men arrested for trying to sell stolen gun parts

Albuquerque Pokemon Go players here to stay and give back

Queen Elizabeth II misses church due to ‘heavy cold’

New UN chief urges New Year’s resolution: ‘Put Peace First’

China says it will shut down ivory trade by end of 2017

Commercial

Guide to Writing Research Papers in APA Style

Why Classroom Allergies Were The Worst

Don’t come to class next week, I am going to be sick. © Greatest College Professor Ever

15 Idiots On Facebook Getting Called Out

Luckily, a colleague of mine had been mining tweets - many of which are from Verified Twitter accounts. So, I took tweets from those Verified accounts and used them as a proxy for non-trolls. 'Verified' became the ninth class in account_category.

The second challenge we had related to classes was that the dataset as a whole is unbalanced. The content volume per class varies greatly; several classes are painfully low in volume.

To quickly and effectively handle this issue we did two things:

Removed critically low account categories - NonEnglish, Fearmonger, and Unknown Developed train_test_split.py to split and balance our dataset

The balancing of the dataset is resolved with the following BigQuery view:

SELECT * FROM ( SELECT * FROM (SELECT account_category, content FROM `project_id.ira_russian_troll_tweets.trans_source_unioned_with_verified_view` WHERE account_category = 'Verified' ORDER BY rand() LIMIT 90000) UNION ALL SELECT * FROM (SELECT account_category, content FROM `project_id.ira_russian_troll_tweets.trans_source_unioned_with_verified_view` WHERE account_category = 'Commercial' ORDER BY rand() LIMIT 90000) UNION ALL SELECT * FROM (SELECT account_category, content FROM `project_id.ira_russian_troll_tweets.trans_source_unioned_with_verified_view` WHERE account_category = 'LeftTroll' ORDER BY rand() LIMIT 90000) UNION ALL SELECT * FROM (SELECT account_category, content FROM `project_id.ira_russian_troll_tweets.trans_source_unioned_with_verified_view` WHERE account_category = 'RightTroll' ORDER BY rand() LIMIT 90000) UNION ALL SELECT * FROM (SELECT account_category, content FROM `project_id.ira_russian_troll_tweets.trans_source_unioned_with_verified_view` WHERE account_category = 'HashtagGamer' ORDER BY rand() LIMIT 90000) UNION ALL SELECT * FROM (SELECT account_category, content FROM `project_id.ira_russian_troll_tweets.trans_source_unioned_with_verified_view` WHERE account_category = 'NewsFeed' ORDER BY rand() LIMIT 90000) ) ORDER BY rand()

The train_test_split.py script is used to create that view. Moreover, this script uses the trans_balanced_view (i.e. trans as in transform) to develop a dataframe, which it subsequently split into the train and test table.

The train-test split is 80/20-80% of the data goes to training, and 20% reserved for testing. The distribution of data across the 'account categories' should remain equal in both the training and test subdatasets.

The ML Pipeline

Scikit-learn Pipeline

For the machine learning model, I chose to use Scikit-learn. I made this choice largely out of a desire to reuse something that I had already developed - what's the point of recreating the wheel if all you want to prove is that the car moves forward?

For those that don't know, pipelines in Scikit-learn allow the coupling of feature extractors (e.g. Count or TF-IDF vectoriser) and ML estimators. In other words, I was able to connect a vectoriser (a way of transforming text into a meaningful array of numbers) with a decision tree classifier (a very simple way of learning to predict the classes of a target feature). This helped me to automate aspects of the data modeling required in each run. Additionally, this allowed me to use the whole text (e.g. a single tweet) as an input rather than the vectorised form of the whole text.

While chaining together a feature extractor with a supervised learning model is relatively simple it effectively demonstrates how to get from raw data input to prediction in a concise manner.

pipeline = Pipeline(

[

("vectorizer", vectorizers[vectorizer])

,(model, models[model][0])

]

) grid_search_cv = GridSearchCV(

estimator=pipline

, param_grid=models[model][1]

, scoring=None

, fit_params=None

, n_jobs=1

, iid=True

, refit=True

, cv=2

, verbose=0

, pre_dispatch='2*n_jobs'

, error_score='raise'

)

Gridsearch

A key aspect of any good ML system is hyperparameter tuning. Through this method, not only can the optimal statistical model be selected but also the optimal feature extractor (at least in Scikit-Learn).

In this project, I chose to use Gridsearch, which is admittedly very brute force. That said, Gridsearch is easy to understand and widely used. It evaluates the performance of all permutations of the model based on all allowed parameters.

See the example below to understand how to limit or expand the parameter options (e.g. vectorizer__ngram_range or 'dtc_model__criterion) that I made available to Gridsearch.

vectorizer_params = {

"vectorizer__ngram_range": [(1,1), (2,2)]

, "vectorizer__stop_words": [None]

} count_vectorizer = CountVectorizer()

tfidf_vectorizer = TfidfVectorizer() vectorizers = {

"count_vectorizer": count_vectorizer

, "tfidf_vectorizer": tfidf_vectorizer

} # Statistical Model

splitter = ['best', 'random']

min_impurity_decrease = [0.0, 0.8]

max_leaf_nodes = [None, 5]

min_samples_leaf = [1, 5]

min_samples_split = [2, 5]

min_weight_fraction_leaf = [0.0, 0.50]

criterion = ['entropy', 'gini']

random_state = [None, 42]

max_features = [None, 'auto']

max_depth = [None, 5]

class_weight = [None] dtc_param = {

'dtc_model__splitter': splitter

, 'dtc_model__min_impurity_decrease': min_impurity_decrease

, 'dtc_model__max_leaf_nodes': max_leaf_nodes

, 'dtc_model__min_samples_leaf': min_samples_leaf

, 'dtc_model__min_samples_split': min_samples_split

, 'dtc_model__min_weight_fraction_leaf': min_weight_fraction_leaf

, 'dtc_model__criterion': criterion #

, 'dtc_model__random_state': random_state

, 'dtc_model__max_features': max_features

, 'dtc_model__max_depth': max_depth

, 'dtc_model__class_weight': class_weight

} for key in vectorizer_params.keys():

dtc_param[key] = vectorizer_params[key]

From the code above, I added both vectoriser and ML model parameters to the parameter dictionary. This allows us to tune our estimator using parameters that are a part of feature extraction, which may have a substantial impact on the best performance of the estimator.

Reality Check

It's important to take a step back. As you can see this is the foundation of a model that can detect what category or class a given tweet likely belongs to; similar vocabulary and word frequency of past tweets in the training dataset help build a model that associates new tweets to that category or class.

The use of frequency of vocabulary as features means this model is limited to shining a light on tweets that resemble past tweets. So, the ML model produced would definitely need to be used in concert with other models to identify individual trolls. Combining models that predict other aspects such as viral range, abusive language, or hateful imagery could help in identifying individual trolls.

This mixed approach is seen in the strategies being examined by researchers and social media companies. In a study, Automated tackling of disinformation, the European Parliamentary Research Service (EPRS) details the factors contributing to the spread of disinformation and explores the possible technological approaches to tackling it. This little project I've worked on would likely fit into the strand of work that is described in section 3.2. Detecting Computational Amplification and Fake Accounts (p33).

If you're interested in the topic of tracking and addressing disinformation and the even broader topic of content monitoring, the following might be worth reading:

What's Next?

Ok, so I have a model. So what!

An ML model does not live in a static environment. This project is a perfect example. Politics and culture change. What people tweet about changes. Models must be retrained to be accurate, relevant, sustainable, and responsible.

So, if I were to put this model into production what would that look like? How do we quickly retrain? How do we track model performance to ensure longevity? How do we deploy to a source where it can be accessed through an API by end-users?

I address those questions in my next article. Stay tuned.

Like what you read? Follow me here on Medium or LinkedIn.