Back-End

15 fev, 2017

Realizando o processo de análise de sentimento do Twitter em Go

Publicidade

Go é uma linguagem que se adequa muito bem as necessidades de backend, tais como processamento concorrente, tarefas agendadas, processamento de arquivos, análise de dados e muito mais.

Neste artigo, iremos tratar de algo muito interessante que é analisar o live stream data do Twitter.

Nosso objetivo é descobrir qual sentimento, positivo ou negativo, que um tweet representa. Para isso, nossa implementação deve ser capaz de classificar automaticamente um tweet com um sentimento positivo ou negativo, de acordo com os termos contidos nele. Desta forma, o nosso classificador precisa ser treinado para tal missão.

Vamos, então, montar uma lista de tweets manualmente classificados por nós.

Vamos começar com 5 tweets positivos e 5 tweets negativos:

Tweets positivos:

  • I love this car.
  • This view is amazing.
  • I feel great this morning.
  • I am so excited about the concert.
  • He is my best friend.

Tweets negativos:

  • I do not like this car.
  • I crash my car.
  • I feel tired this morning.
  • I am not looking forward to the concert.
  • He is my enemy.

Em uma implementação mais completa, deve ser utilizado um número bem maior de tweets para treinar o classificador. Quanto mais bem treinado, melhor é o resultado apresentado.

Então, mãos a obra…

Este será nosso JSON contendo nossa base de treinamento:

twittersentimentclassifier.json

[
    {
        "Tweet": "I love this car",
        "Classifier": "positive"
    },
    {
        "Tweet": "This view is amazing",
        "Classifier": "positive"
    },
    {
        "Tweet": "I feel great this morning",
        "Classifier": "positive"
    },
    {
        "Tweet": "I am so excited about the concert",
        "Classifier": "positive"
    },
    {
        "Tweet": "He is my best friend",
        "Classifier": "positive"
    },
    {
        "Tweet": "I do not like this car",
        "Classifier": "negative"
    },
    {
        "Tweet": "I crash my car",
        "Classifier": "negative"
    },
    {
        "Tweet": "I feel tired this morning",
        "Classifier": "negative"
    },
    {
        "Tweet": "I am not looking forward to the concert",
        "Classifier": "negative"
    },
    {
        "Tweet": "He is my enemy",
        "Classifier": "negative"
    }
]

Para armazenar este JSON, precisamos da seguinte struct:

//TwitterSentimentClassifier : Tweets para treinar o classificador.
type TwitterSentimentClassifier struct {
	Tweet      string
	Classifier string
}

E para realizar a leitura do arquivo JSON, teremos a seguinte rotina:

func getTwitterSentimentClassifier(file string) []TwitterSentimentClassifier {
	//Realiza a leitura do arquivo json
	raw, err := ioutil.ReadFile(file)

	//Tratamento de erros padrão.
	if err != nil {
		fmt.Println(err.Error())
		os.Exit(1)
	}
	var twitterSentimentClassifier []TwitterSentimentClassifier

	//Unmarshal do conteúdo do arquivo json para um tipo struct TwitterSentimentClassifier
	json.Unmarshal(raw, &twitterSentimentClassifier)
	return twitterSentimentClassifier
}

Devemos ter a seguinte leitura do arquivo JSON:

Agora que temos nossa lista, iremos transformá-la em tuplas contendo dois elementos. O primeiro elemento é um array contendo os termos de cada tweet e segundo elemento é o tipo classificação, ou seja, nossa classificação positiva e negativa.

Para isso precisaremos destas duas struct:

//TermClassified : Termos classificados.
type TermClassified struct {
	Term     string
	FreqDist int
}

//TermClassifier : Classificador dos Termos dos tweets para treinar o classificador
type TermClassifier struct {
	Term       []string
	Classifier string
}

Vamos criar agora nossa rotina responsável por transformar os tweets em nossa lista de termos classificados:

func getTermClassifier(twitterSentimentClassifier []TwitterSentimentClassifier) (int, []TermClassifier) {

	termClassifier := make([]TermClassifier, len(twitterSentimentClassifier))
	var generalCount int
	for i, item := range twitterSentimentClassifier {

		//Primeiro vamos fazer através da função Fields o split da sentença por espaços
		tweet := strings.Fields(item.Tweet)

		//Criamos um slice do tipo Term do tamanho máximo dos splits do nosso tweet.
		term2Classifier := make([]string, len(tweet))

		var count int
		for j, termTweet := range tweet {
			//Estamos considerando apenas palavras maiores que três caracteres para serem consideradas como termos válidos
			//Utilizamos rune para prevenir caracteres especiais, acentos, caracteres asiáticos e também emogis
			if len([]rune(termTweet)) >= 3 {
				term2Classifier[j] = strings.ToLower(termTweet)
				count++
			}
		}

		termClassifier[i].Term = make([]string, count)
		count = 0
		for k, termTweetClassifier := range term2Classifier {
			//Realizamos um ajuste do tamanho da slice final de termos
			if term2Classifier[k] != "" {
				termClassifier[i].Term[count] = termTweetClassifier
				count++
				generalCount++
			}
		}

		termClassifier[i].Classifier = item.Classifier
	}

	return generalCount, termClassifier
}

O resultado deverá ser este:

Agora iremos verificar quantas vezes cada termo aparece em nossos tweets de treinamento, ou seja, frequência de distribuição ou simplesmente score.

Vamos convencionar que os termos encontrados em tweets que classificamos com o sentimento positivo recebem + 1 e que os termos encontrados em tweets que classificamos com o sentimento negativo recebem -1.

func afterClassifier(generalCount int, termClassifier []TermClassifier) []TermClassified {
	//Esta slice receberá todos os termos identificados na slice anterior
	termClassified := make([]TermClassified, generalCount)

	var count int
	var countTermAfterClassifier int
	for _, item := range termClassifier {
		for _, itemTerm := range item.Term {

			//Antes de aplicar o score em um termo verificamos se ele já não fora identificado anteriormente.
			//Caso este termo já tenha sido identificado apenas contabilizamos a frequência de distribuição (score)
			var found bool
			for _, itemTermForCompareBeforeInsert := range termClassified {
				if itemTermForCompareBeforeInsert.Term == itemTerm {
					found = true
					break
				}
			}
			if !found {
				termClassified[count].Term = itemTerm
				countTermAfterClassifier++
				//Agora iremos aplicar a frequência de distribuição (score) de cada termo em relação ao sentimento que demos em cada um dos tweets
				for _, itemForCompare := range termClassifier {
					for _, itemTermToCompare := range itemForCompare.Term {

						if itemTerm == itemTermToCompare {
							if itemForCompare.Classifier == "positive" {
								termClassified[count].FreqDist = termClassified[count].FreqDist + 1
							} else if itemForCompare.Classifier == "negative" {
								termClassified[count].FreqDist = termClassified[count].FreqDist - 1
							}

						}
					}
				}
			}

			count++
		}
	}

	// Removendo registros vazios
	termAfterClassifierClassified := make([]TermClassified, countTermAfterClassifier)
	var countAfterClassifierClassified int
	for _, itemTermClassified := range termClassified {
		if itemTermClassified.Term != "" {
			termAfterClassifierClassified[countAfterClassifierClassified] = itemTermClassified
			countAfterClassifierClassified++
		}

	}

	return termAfterClassifierClassified
}

O resultado deve ser este:

Como exemplo, percebam que o termo “car” está com score -1, pois aparece em um tweet que classificamos como sendo positivo e em dois tweets que classificamos como sendo negativo, logo (+1)(-1)(-1) = -1

Lista completa:

Nosso próximo passo será criarmos uma lista JSON com tweets livres de classificação e submetê-los a uma classificação de termos onde nosso classificador irá determinar o sentimento de cada tweet com base no score de seus termos.

Para facilitar nossa experiência, iremos utilizar a mesma listagem de tweets que utilizamos para balizar o classificador; apenas criaremos um novo arquivo JSON e remover a classificação “positive” e “negative”. Veja o exemplo:

twitter.json

{
        "Tweet": "I love this car",
        "Classifier": ""
    },
    {
        "Tweet": "This view is amazing",
        "Classifier": ""
    }, …

Iremos reaproveitar os mesmos métodos que utilizamos até agora para trabalhar com os tweets.

getTermClassifier(getTwitterSentimentClassifier("./twitter.json"))

Devemos ter um resultado assim:

Notem que a classificação de todos os tweets estão vazias.

Nosso classificador deverá, então, analisar cada tweet e baseado no score de cada termo e classificar este tweet como sendo positivo ou negativo.

func main() {

	lst1 := afterClassifier(getTermClassifier(getTwitterSentimentClassifier("./twittersentimentclassifier.json")))
	_, lst2 := getTermClassifier(getTwitterSentimentClassifier("./twitter.json"))

	for i, item := range lst2 {
		var count = 0
		for _, itemTerm := range item.Term {
			for _, itemClassifier := range lst1 {
				if itemTerm == itemClassifier.Term {
					//Aqui definimos qual frequência de distribuição (score) será considerado como um sentimento positivo ou negativo
					if itemClassifier.FreqDist >= 0 {
						count++
					} else {
						count--
					}
				}
			}

		}
		if count >= 0 {
			lst2[i].Classifier = "positive"
		} else {
			lst2[i].Classifier = "negative"
		}
	}

	_, originalTweetList := getTermClassifier(getTwitterSentimentClassifier("./twittersentimentclassifier.json"))

	outLine("Tweets originais:", originalTweetList)
	outLine("Tweets reclassificados:", lst2)
}

E o resultado da classificação dos tweets gerada pelo nosso classificador será este:

Notem que mesmo utilizando exatamente a mesma lista, algumas classificações se mantiveram, enquanto outras ganharam outro valor com base no que nosso classificador aprendeu em seu treinamento.

Tweets originais:

  • love this car – positive
  • this view amazing – positive
  • feel great this morning – positive
  • excited about the concert – positive
  • best friend – positive
  • not like this car – negative
  • crash car – negative
  • feel tired this morning – negative
  • not looking forward the concert – negative
  • enemy – negative

Tweets reclassificados:

  • love this car – positive
  • this view amazing – positive
  • feel great this morning – positive
  • excited about the concert – positive
  • best friend – positive
  • not like this car – negative
  • crash car – negative
  • feel tired this morning – positive
  • not looking forward the concert – negative
  • enemy – negative

Onde nossa divergência apresenta os seguintes scores em cada termo:

Logo, o nosso classificador atribuiu score 0 para este tweet e, pela nossa regra, score 0 é igual a sentimento positivo.

PLUS…

Agora, vamos trabalhar com tweets reais!

Primeiro, vamos criar um serviço de streaming do Twitter e depois deixar nosso classificador trabalhar em cima de dados reais…

Para criar ID, chaves e token do aplicativo Twiiter:

  1. Entre no aplicativo do Twitter e clique no link Sign in ou Sign up now se você não tem uma conta do Twitter;
  2. Clique em Criar Novo Aplicativo;
  3. Insira um Nome, Descrição e Site. O nome do aplicativo Twitter deve ser um nome exclusivo. O campo site da Web na verdade não é usado. Ele não precisa ser uma URL válida;
  4. Marque sim, eu li e concordo com o Developer Agreement. Em seguida, clique em Criar seu aplicativo do Twitter;
  5. Clique na guia Permissões. A permissão padrão é somente leitura. Isso é suficiente para este artigo;
  6. Clique na guia Chaves e Tokens de acesso;
  7. Clique em Criar meu token de acesso;
  8. Copie os valores-chave do consumidor, segredo do consumidor, token de acesso e segredo do token de acesso.

Agora que já temos as chaves necessárias, iremos utilizar uma biblioteca chamada anaconda “https://github.com/ChimeraCoder/anaconda”. Anaconda é uma biblioteca cliente para a API 1.1 do Twitter.

Para tal, iremos adicionar os seguintes pacotes ao nosso programa:

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"strings"
	"anaconda"
	"net/url"
)

Lembrem-se de clonar a biblioteca “git clone https://github.com/ChimeraCoder/anaconda.git” para o src do seu %GOPATH% e depois disso dê o comando “go get” dentro da sua clonagem para poder seguir com o consumo desta biblioteca.

Com os pacotes adicionados, vamos criar a seguinte função:

func twitterStreamingAPI() []TwitterSentimentClassifier {
	//Mais sobre: https://github.com/ChimeraCoder/anaconda

	anaconda.SetConsumerKey("xxx")                                                                                     //Consumer Key
	anaconda.SetConsumerSecret("xxx")                                                             //Consumer Secret
	client := anaconda.NewTwitterApi("xxx", "xxx") //Access Token, Access Token Secret

	// setando os parametros utilizando url.Values
	v := url.Values{}
	v.Set("count", "30") // ou v.Set("locations", "<Locations>")
	result, err := client.GetSearch("golang", nil)//buscar por tweets que contenham o termo “golang”
	if err != nil {
		fmt.Println(err.Error())
		os.Exit(1)
	}

	// Ao menos que exista algo estranho, devemos ter ao menos 2 tweets
	if len(result.Statuses) < 2 {
		fmt.Printf("Esperado 2 ou mais tweets, foram encontrados %d", len(result.Statuses))
		os.Exit(1)
	}

	twitterSentimentClassifier := make([]TwitterSentimentClassifier, len(result.Statuses))

	// verificar a existência de tweet vazio
	for i, tweet := range result.Statuses {
		twitterSentimentClassifier[i].Tweet = tweet.Text
	}

	return twitterSentimentClassifier
}

E para finalizar, vamos simplesmente enviar para a rotina getTermClassifier os tweets que coletamos da stream.

_, lst2 := getTermClassifier(twitterStreamingAPI())

Sem mudanças adicionais no processamento que criamos teremos classificações reais como estas:

@turnoff_us: the depressed developer #comic #golang #depresseddeveloper @golang 
https://t.co/7rwtf3ylus https://t.co/1ppyi5fzol  - positive 
@turnoff_us: the depressed developer #comic #golang #depresseddeveloper @golang https://t.co/7rwtf3ylus https://t.co/1ppyi5fzol  - positive 
@andychilton: true. said this *so* *many* *times* about #nodejs, specifically express. was one reason broke. still happy swi… 
 - positive 
@srcgraph: new "used by" badges from @srcgraph you can see how many people use your @golang library https://t.co/kvgi4pynej  - positive 
@turnoff_us: the depressed developer #comic #golang #depresseddeveloper @golang https://t.co/7rwtf3ylus https://t.co/1ppyi5fzol  - positive

Legal, né?

Então, a ideia aqui foi mostrar que com as funções básicas de Go e de maneira bastante simples, podemos construir coisas incríveis.

Também deve ter ficado claro que quanto maior a nossa base de dados utilizada no treinamento, melhor será a analise feita pelo nosso classificador.

Certamente podemos ter inúmeras melhorias neste código, tal como o uso de goroutines, e com base nas classificações, identificar o sentimento sobre o termo de pesquisa, evolução nos ranges de classificação, melhorias nas iterações etc… Mas isso é com vocês!

Conclusão: Go é muito divertido!

Obs.: Git: https://github.com/edwardmartinsjr/iMasters-Go/tree/master/twittersentiment