天天看點

微網誌情感分析應用實戰【Streamlit + Flair】

Streamlit是一個出色的機器學習工具開發庫,這個教程将學習如何使用streamlit和flair開發一個twitter微網誌情感分析的應用。

相關連結: Streamlit開發手冊

1、streamlit概述

并不是每個人都是資料科學家,但是每個人都需要資料科學帶來的力量。Streamlit幫我們解決了這個問題,利用streamlit部署機器學習模型簡單到隻需要幾個函數調用。

例如,如果運作下面的代碼:

import streamlit as st

x = st.slider('Select a value')
st.write(x, 'squared is', x * x)           

Streamlit就會建立出像下面這樣的滑杆輸入:

微網誌情感分析應用實戰【Streamlit + Flair】

安裝Streamlit也很簡單:

pip3 install streamlit           

然後你就可以運作應用了:

streamlit run <FILE>           

注意,直接用python運作你的streamlit檔案是不行的!

本文中的代碼可以到

這裡

下載下傳。

2、情感分類

情感分類是自然語言處理(NLP)中的一個經典問題,目的是判斷一個語句的情感傾向是積極(Positive)還是消極(Negative)。

例如,“I love Python!”這句話應當被歸類為Positive,而“Python is the worst!”則應當被歸類為Negative。

3、Flair開發庫

很多流行的機器學習開發庫都提供了情感分類器的實作,從簡單和效果方面考慮,在這個教程裡我們使用Flair,一個頂級的NLP分類器開發包。

可以執行如下指令安裝Flair:

pip3 install flair           

4、Sentiment140資料集

任何資料科學項目都需要資料集,Sentiment140資料集是我們這個項目的絕配。該資料集包含了160萬條标注好的tweet微網誌,标注0表示消極,4表示積極。

可以從這裡下載下傳

Sentiment140資料集

5、資料載入及預處理

一旦下載下傳好Sentiment140資料集,就可以使用如下代碼載入資料:

import pandas as pd

col_names = ['sentiment','id','date','query_string','user','text']
data_path = 'training.1600000.processed.noemoticon.csv'

tweet_data = pd.read_csv(data_path, header=None, names=col_names, encoding="ISO-8859-1").sample(frac=1) # .sample(frac=1) shuffles the data
tweet_data = tweet_data[['sentiment', 'text']] # Disregard other columns
print(tweet_data.head())           

運作上面的代碼将輸出如下結果:

sentiment                                               text
1459123          4  @minalpatel Any more types of glassware you'd...
544833           0  I was a bit puzzled as to why it seemed to it...
398665           0  Yay...my car is ready....Was about 2500 miles...
708548           0               @JoshEJosh How ya been? I MISS you! 
264000           0  @MrFresh0587 yeah i know. well...i'm going to...           

不過,因為我們使用

.sample(frac=1)

随機打亂了資料的先後次序,你得到的結果可能略有不同。

現在資料還很亂,我們先進行預處理:

import re

allowed_chars = ' AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789~`!@#$%^&*()-=_+[]{}|;:",./<>?'
punct = '!?,.@#'
maxlen = 280

def preprocess(text):
    return ''.join([' ' + char + ' ' if char in punct else char for char in [char for char in re.sub(r'http\S+', 'http', text, flags=re.MULTILINE) if char in allowed_chars]])[:maxlen]           

上面的函數略為有點乏味,但是簡而言之,這段代碼的目的是剔除文本中所有不能識别的字元、連結等并截斷為280個字元。有更好的辦法來進行連結清理等預處理,不過我們這裡就用最樸素的方法了。

Flair對資料格式有特定的要求,看起來是這樣:

__label__<LABEL>    <TEXT>           

在我們的微網誌情感分析應用中,資料整理後應該是這樣:

__label__4    <PRE-PROCESSED TWEET>
__label__0    <PRE-PROCESSED TWEET>
...           

為此,我們需要三個步驟:

1、執行預處理函數

tweet_data['text'] = tweet_data['text'].apply(preprocess)           

2、在每個情感标記前添加

__label__

字首

tweet_data['sentiment'] = '__label__' + tweet_data['sentiment'].astype(str)           

3、儲存資料

import os

# Create directory for saving data if it does not already exist
data_dir = './processed-data'
if not os.path.isdir(data_dir):
    os.mkdir(data_dir)

# Save a percentage of the data (you could also only load a fraction of the data instead)
amount = 0.125

tweet_data.iloc[0:int(len(tweet_data)*0.8*amount)].to_csv(data_dir + '/train.csv', sep='\t', index=False, header=False)
tweet_data.iloc[int(len(tweet_data)*0.8*amount):int(len(tweet_data)*0.9*amount)].to_csv(data_dir + '/test.csv', sep='\t', index=False, header=False)
tweet_data.iloc[int(len(tweet_data)*0.9*amount):int(len(tweet_data)*1.0*amount)].to_csv(data_dir + '/dev.csv', sep='\t', index=False, header=False)           

在上面的代碼中,你可能注意到了兩個問題:

  • 我們僅儲存了部分資料。這是因為Sentiment140資料集太大了,如果Flair加載 完整的資料集需要太多的記憶體。
  • 我們将資料分割為訓練集、測試集和開發集。當Flair載入資料時,它需要資料 按這種方法拆分。預設情況下,拆分比例為8-1-1,即80%的資料進訓練集、10% 的資料進測試集、10%的資料進開發集

現在,資料準備好了!

6、基于Flair的文本情感分類實作

在這個教程中,我們僅涉及Flair的基礎。如果你需要更多細節,推薦你檢視Flair的官方文檔。

首先我們用Flair的NLPTaskDataFetcher 類載入資料:

from flair.data_fetcher import NLPTaskDataFetcher
from pathlib import Path

corpus = NLPTaskDataFetcher.load_classification_corpus(Path(data_dir), test_file='test.csv', dev_file='dev.csv', train_file='train.csv')           

然後我們構造一個标簽字典來記錄語料庫中配置設定給文本的所有标簽:

label_dict = corpus.make_label_dictionary()           

現在可以載入Flair内置的GloVe詞嵌入了:

from flair.embeddings import WordEmbeddings, FlairEmbeddings

word_embeddings = [WordEmbeddings('glove'),
#                    FlairEmbeddings('news-forward'),
#                    FlairEmbeddings('news-backward')
                  ]           

注釋掉的兩行代碼是Flair提供的選項,用于得到更好的效果,不過我的記憶體有限,是以無法進行測試。

載入詞嵌入向量後,用下面的代碼進行初始化:

from flair.embeddings import DocumentRNNEmbeddings

document_embeddings = DocumentRNNEmbeddings(word_embeddings, hidden_size=512, reproject_words=True, reproject_words_dimension=256)           

現在整合詞嵌入向量和标簽字典,得到一個TextClassifier模型:

from flair.models import TextClassifier

classifier = TextClassifier(document_embeddings, label_dictionary=label_dict)           

接下來我們可以建立一個ModelTrainer執行個體來用我們的語料庫訓練模型:

from flair.trainers import ModelTrainer

trainer = ModelTrainer(classifier, corpus)           

一旦開始訓練,我們需要等一會兒了:

trainer.train('model-saves',
              learning_rate=0.1,
              mini_batch_size=32,
              anneal_factor=0.5,
              patience=8,
              max_epochs=200)           

在模型訓練完之後,可以使用如下的代碼進行測試:

from flair.data import Sentence

classifier = TextClassifier.load('model-saves/final-model.pt')

pos_sentence = Sentence(preprocess('I love Python!'))
neg_sentence = Sentence(preprocess('Python is the worst!'))

classifier.predict(pos_sentence)
classifier.predict(neg_sentence)

print(pos_sentence.labels, neg_sentence.labels)           

你應該可以得到類似下面這樣的結果:

[4 (0.9758405089378357)] [0 (0.8753706812858582)]           

看起來預測是正确的!

7、抓取twitter微網誌

不錯,現在我們有了一個可以預測單條tweet的感情色彩是積極或消極。不過這還不是太有用,那麼應該怎麼改進?

我的想法是抓取指定查詢條件的最新tweet微網誌,逐個進行情感分類,然後計算積極/消極的比率。

我個人喜歡用twitterscraper來抓twitter微網誌,雖然它不算快,但你可以繞過twitter設定的請求限制。用下面的指令安裝twitterscraper:

pip3 install twitterscraper           

安裝好了。稍後我們再進行具體的抓取。

8、編寫Streamlit腳本

建立一個新的檔案main.py,然後先引入一些子產品:

import datetime as dt
import re

import pandas as pd
import streamlit as st
from flair.data import Sentence
from flair.models import TextClassifier
from twitterscraper import query_tweets           

接下來,我們可以進行一些基本的處理,例如設定頁面标題、載入分類模型:

# Set page title
st.title('Twitter Sentiment Analysis')

# Load classification model
with st.spinner('Loading classification model...'):
    classifier = TextClassifier.load('models/best-model.pt')           

with st.spinner

這部分代碼塊讓我們可以在加載分類模型時給使用者一個進度提示。

接下來我們可以複制之前寫的預處理函數:

import re

allowed_chars = ' AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789~`!@#$%^&*()-=_+[]{}|;:",./<>?'
punct = '!?,.@#'
maxlen = 280

def preprocess(text):
    return ''.join([' ' + char + ' ' if char in punct else char for char in [char for char in re.sub(r'http\S+', 'http', text, flags=re.MULTILINE) if char in allowed_chars]])[:maxlen]           

我們首先實作單個tweet微網誌的分類:

st.subheader('Single tweet classification')

tweet_input = st.text_input('Tweet:')           

隻要輸入文本不是空的,我們就進行如下處理:

  • 預處理tweet微網誌
  • 進行預測
  • 顯式預測結果
if tweet_input != '':
    # Pre-process tweet
    sentence = Sentence(preprocess(tweet_input))

    # Make predictions
    with st.spinner('Predicting...'):
        classifier.predict(sentence)

    # Show predictions
    label_dict = {'0': 'Negative', '4': 'Positive'}

    if len(sentence.labels) > 0:
        st.write('Prediction:')
        st.write(label_dict[sentence.labels[0].value] + ' with ',
                sentence.labels[0].score*100, '% confidence')           

使用

st.write

可以寫入任何文本,甚至可以直接顯式Pandas資料幀。

好了,現在可以運作:

streamlit run main.py           

結果看起來是這樣:

微網誌情感分析應用實戰【Streamlit + Flair】

接下來我們可以實作之前的想法了:搜尋某個主題的twitter微網誌并計算情感正負比。

st.subheader('Search Twitter for Query')

# Get user input
query = st.text_input('Query:', '#')

# As long as the query is valid (not empty or equal to '#')...
if query != '' and query != '#':
    with st.spinner(f'Searching for and analyzing {query}...'):
        # Get English tweets from the past 4 weeks
        tweets = query_tweets(query, begindate=dt.date.today() - dt.timedelta(weeks=4), lang='en')

        # Initialize empty dataframe
        tweet_data = pd.DataFrame({
            'tweet': [],
            'predicted-sentiment': []
        })

        # Keep track of positive vs. negative tweets
        pos_vs_neg = {'0': 0, '4': 0}

        # Add data for each tweet
        for tweet in tweets:
            # Skip iteration if tweet is empty
            if tweet.text in ('', ' '):
                continue
            # Make predictions
            sentence = Sentence(preprocess(tweet.text))
            classifier.predict(sentence)
            sentiment = sentence.labels[0]
            # Keep track of positive vs. negative tweets
            pos_vs_neg[sentiment.value] += 1
            # Append new data
            tweet_data = tweet_data.append({'tweet': tweet.text, 'predicted-sentiment': sentiment}, ignore_index=True)           

最後,我們顯示采集的資料:

try:
    st.write(tweet_data)
    # Show positive to negative tweet ratio
    try:
        st.write('Positive to negative tweet ratio:', pos_vs_neg['4']/pos_vs_neg['0'])
    except ZeroDivisionError: # if no negative tweets
        st.write('All postive tweets')
except NameError: # if no queries have been made yet
    pass           

再次運作應用,結果如下:

微網誌情感分析應用實戰【Streamlit + Flair】

下面我們完整的streamlit應用腳本:

import datetime as dt
import re

import pandas as pd
import streamlit as st
from flair.data import Sentence
from flair.models import TextClassifier
from twitterscraper import query_tweets

# Set page title
st.title('Twitter Sentiment Analysis')

# Load classification model
with st.spinner('Loading classification model...'):
    classifier = TextClassifier.load('models/best-model.pt')

# Preprocess function
allowed_chars = ' AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789~`!@#$%^&*()-=_+[]{}|;:",./<>?'
punct = '!?,.@#'
maxlen = 280

def preprocess(text):
    # Delete URLs, cut to maxlen, space out punction with spaces, and remove unallowed chars
    return ''.join([' ' + char + ' ' if char in punct else char for char in [char for char in re.sub(r'http\S+', 'http', text, flags=re.MULTILINE) if char in allowed_chars]])

### SINGLE TWEET CLASSIFICATION ###
st.subheader('Single tweet classification')

# Get sentence input, preprocess it, and convert to flair.data.Sentence format
tweet_input = st.text_input('Tweet:')

if tweet_input != '':
    # Pre-process tweet
    sentence = Sentence(preprocess(tweet_input))

    # Make predictions
    with st.spinner('Predicting...'):
        classifier.predict(sentence)

    # Show predictions
    label_dict = {'0': 'Negative', '4': 'Positive'}

    if len(sentence.labels) > 0:
        st.write('Prediction:')
        st.write(label_dict[sentence.labels[0].value] + ' with ',
                sentence.labels[0].score*100, '% confidence')

### TWEET SEARCH AND CLASSIFY ###
st.subheader('Search Twitter for Query')

# Get user input
query = st.text_input('Query:', '#')

# As long as the query is valid (not empty or equal to '#')...
if query != '' and query != '#':
    with st.spinner(f'Searching for and analyzing {query}...'):
        # Get English tweets from the past 4 weeks
        tweets = query_tweets(query, begindate=dt.date.today() - dt.timedelta(weeks=4), lang='en')

        # Initialize empty dataframe
        tweet_data = pd.DataFrame({
            'tweet': [],
            'predicted-sentiment': []
        })

        # Keep track of positive vs. negative tweets
        pos_vs_neg = {'0': 0, '4': 0}

        # Add data for each tweet
        for tweet in tweets:
            # Skip iteration if tweet is empty
            if tweet.text in ('', ' '):
                continue
            # Make predictions
            sentence = Sentence(preprocess(tweet.text))
            classifier.predict(sentence)
            sentiment = sentence.labels[0]
            # Keep track of positive vs. negative tweets
            pos_vs_neg[sentiment.value] += 1
            # Append new data
            tweet_data = tweet_data.append({'tweet': tweet.text, 'predicted-sentiment': sentiment}, ignore_index=True)

# Show query data and sentiment if available
try:
    st.write(tweet_data)
    try:
        st.write('Positive to negative tweet ratio:', pos_vs_neg['4']/pos_vs_neg['0'])
    except ZeroDivisionError: # if no negative tweets
        st.write('All postive tweets')
except NameError: # if no queries have been made yet
    pass           

原文連結:

Streamlit+Flair開發微網誌情感分析應用 — 彙智網