NLP笔记Day2:词频统计及可视化

在本次练习中,我们主要实现build_freqs()函数,并且把数据喂进去看看可视化的结果。在整个推特情感分析项目中,这个函数的任务是构建一个字典。我们可以在字典里面查找每个词出现的次数。字典对于后续提取数据集的特征值是非常有帮助的。

不单单是计算频次,而是计算一个单词,描述正向的次数和负向的次数。也就是说,当一个单词出现在一个句子时,这个句子更可能是在讲正向的话,还是负向的。

导入库

先来导入我们需要用到的库

import nltk
from nltk.corpus import twitter_samples
import matplotlib.pyplot as plt
import numpy as np

导入两个在utils.py中定义好的函数

  • process_tweet() 数据清洗,分词函数
  • build_freqs() 计算一个单词关联正向或负向的频次,然后构建出 freqs 字典,其中的数据键是 (word, label),数据值是频次(一个数值)
from nltk.book import *
from utils import process_tweet, build_freqs
*** Introductory Examples for the NLTK Book ***
Loading text1, ..., text9 and sent1, ..., sent9
Type the name of the text or sentence to view it.
Type: 'texts()' or 'sents()' to list the materials.
text1: Moby Dick by Herman Melville 1851
text2: Sense and Sensibility by Jane Austen 1811
text3: The Book of Genesis
text4: Inaugural Address Corpus
text5: Chat Corpus
text6: Monty Python and the Holy Grail
text7: Wall Street Journal
text8: Personals Corpus
text9: The Man Who Was Thursday by G . K . Chesterton 1908

加载 NLTK 样本数据集

像前一篇做数据预处理的步骤一样,先把数据加载到列表中来。在这个练习当中,加载数据非常简单,只需要一行命令。但我们在实际情况中会碰到多种数据源,比如:关系型数据库,非关系型数据库,接口如WebSerivices,文本,excel表格等等。因此许多从业人员也会抱怨,做数据科学80~90%的时间都花在了处理数据本身之上。

all_positve_tweets = twitter_samples.strings('positive_tweets.json')
all_negative_tweets = twitter_samples.strings('negative_tweets.json')

tweets = all_positve_tweets + all_negative_tweets

print("Number of tweets", len(tweets))
Number of tweets 10000

接下来,我们会创建一个标签数组(array),后续用于关联实际数据的情感。数组的使用跟列表差不多,但会更便于计算。
这里labels数组共10000个元素,前5000个全是 ‘1‘ 代表正向情感,后5000个全是 ‘0‘ 代表负向。
我们用numpy库来进行这样的处理。

  • np.ones() 创建全 1 数组
  • np.zeros() 创建全 0 数组
  • np.append() 连接数组
# 分别给 np.ones() 和 np.zeros() 输入两个list的长度值,然后再连接到一块去
labels = np.append(np.ones((len(all_positve_tweets))), np.zeros((len(all_negative_tweets))))
# 显示出来看看更直观
labels
array([ 1.,  1.,  1., ...,  0.,  0.,  0.])

构建字典

在Python,字典是可变的,被索引好的集合。它把每一个成员存储为 键-值对(key-value pairs),并使用哈希表来建立查找索引。这有利于在上千条数据中进行快速检索。

定义

Python字典的声明使用大括号

dictionary = {'key1': 1, 'key2': 2}

这里定义的字典包含两个条目,在这个例子里,我们使用了字符串类型,根据实际情况还可以是浮点数,整数,元组(tuple)等

增加/修改入口

用中括号可以对字典进行操作,如果字典键已存在,值会被覆盖

# 增加新入口
dictionary['key3'] = -5

# 覆盖值
dictionary['key1'] = 0

print(dictionary)
{'key1': 0, 'key2': 2, 'key3': -5}

访问值和查找键

对字典进行检索和提取是常用操作,建议反复练习,这里有两个方法:

  • 使用中括号:键已存在时用,若键不存在则报错
  • 使用get()函数:如果值不存在,则可以存在一个默认值
# 中括号查找
print(dictionary['key2'])
2

我们来试试报错的情况,查找一个不存在的键

print(dictionary['key666'])
---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

<ipython-input-8-3e8f34e21792> in <module>()
----> 1 print(dictionary['key666'])


KeyError: 'key666'

在使用中括号进行检索时,一个常用的办法是通过条件语句来处理键是否能被找到的问题。或者你也可以使用get()函数来为找不到的键生成一个默认值。

if 'key1' in dictionary:
    print("item found: ", dictionary['key1'])  # 输出 key1 对应的值为:0
else:
    print('key1 is not defined')

print("item found: ", dictionary.get('key1', -1)) # 同上,若 key1 没有对应值才会输出 -1 
item found:  0
item found:  0
if 'key7' in dictionary:
    print(dictionary['key7'])
else:
    print('key does not exists') # 字典中没有 key7 因此会输出这一行

print(dictionary.get('key7', -1)) # 字典中没有 key7 所以会直接给 key7 一个默认值为 -1
print(dictionary['key7']) # 最后的报错意味着上一步get()的执行并没有给字典赋值
key does not exists
-1



---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

<ipython-input-10-ff9ae431bf94> in <module>()
      5 
      6 print(dictionary.get('key7', -1)) # 字典中没有 key7 所以会直接给 key7 一个默认值为 -1
----> 7 print(dictionary['key7']) # 最后的报错意味着上一步get()的执行并没有给字典赋值


KeyError: 'key7'

词频统计

下面我们一起来看看 build_freqs() 函数是如何实现的

def build_freqs(tweets, ys):
    """Build frequencies.
    Input:
        tweets: a list of tweets
        ys: an m x 1 array with the sentiment label of each tweet
            (either 0 or 1)
    Output:
        freqs: a dictionary mapping each (word, sentiment) pair to its
        frequency
    """
    # Convert np array to list since zip needs an iterable.
    # The squeeze is necessary or the list ends up with one element.
    # Also note that this is just a NOP if ys is already a list.
    yslist = np.squeeze(ys).tolist()

    # Start with an empty dictionary and populate it by looping over all tweets
    # and over all processed words in each tweet.
    freqs = {}
    for y, tweet in zip(yslist, tweets):
        for word in process_tweet(tweet):
            pair = (word, y)
            if pair in freqs:
                freqs[pair] += 1
            else:
                freqs[pair] = 1
    
    return freqs

如前所述,你也可以使用get()函数处理键值匹配问题,如:

    for y, tweet in zip(yslist, tweets):
        for word in process_tweet(tweet):
            pair = (word, y)
            freqs[pair] = freqs.get(pair, 0) + 1

如上所示,(word, y) 是一对键-值。word是被处理后的推特数据,而y则是对应正向/负向的标记,即1或0。例如:

    # "folowfriday" 在正向推特中出现了 25 次
    ('followfriday', 1.0): 25

    # "shame" 在负向推特中出现了 19 次
    ('shame', 0.0): 19 

到这里,或者你就能够理解自然语言为何如此难,因为人类表达中独有的诸如先抑后扬,用负面词汇表达正向情感等。
例如,一款好玩的游戏,我们常常会说“有毒”,但“有毒”这一词在通常情况下又是负面的。
人类语言(特别是中文)这样的特性让机器“理解”语言变得尤为困难。

下面,我们就来看看build_freqs()函数返回的字典长什么样子

freqs = build_freqs(tweets, labels)

print(f'type(freqs) = {type(freqs)}')  # print的一种用法,把大括号执行语句的输出,跟前面的字符串合并起来

print(f'len(freqs) = {len(freqs)}')
type(freqs) = <class 'dict'>
len(freqs) = 13076

再把所有词的词频print出来看看

print(freqs)
{('followfriday', 1.0): 25, ......('misser', 0.0): 1}

然而,这样的结果对于理解数据并没有带来太多帮助。更好的办法是对数据进行可视化。

词频表格

在可视化之后,把我们感兴趣的那部分数据放进一张临时表(table)是更好的办法。

keys = ['happi', 'merri', 'nice', 'good', 'bad', 'sad', 'mad', 'best', 'pretti',
        '❤', ':)', ':(', '😒', '😬', '😄', '😍', '♛',
        'song', 'idea', 'power', 'play', 'magnific']

data = []

for word in keys:

    pos = 0
    neg = 0

    if (word, 1) in freqs:
        pos = freqs[(word, 1)]
    
    if (word, 0) in freqs:
        neg = freqs[(word, 0)]
    
    data.append([word, pos, neg])

data
[['happi', 211, 25],
 ['merri', 1, 0],
 ['nice', 98, 19],
 ['good', 238, 101],
 ['bad', 18, 73],
 ['sad', 5, 123],
 ['mad', 4, 11],
 ['best', 65, 22],
 ['pretti', 20, 15],
 ['❤', 29, 21],
 [':)', 3568, 2],
 [':(', 1, 4571],
 ['😒', 1, 3],
 ['😬', 0, 2],
 ['😄', 5, 1],
 ['😍', 2, 1],
 ['♛', 0, 210],
 ['song', 22, 27],
 ['idea', 26, 10],
 ['power', 7, 6],
 ['play', 46, 48],
 ['magnific', 2, 0]]

这张表格看起来就清晰多了,更进一步,我们可以使用散点图来表示这些数据。
我们不用原始数值来绘制,因为那会导致图样范围太大,取而代之我们在对数尺度上绘制,这样所有的计数更能够在一个维度上看清。

例如,“:)” 有3568个计数为正,只有2个为负,对数取值后变为8.17和0.69。(想通过计算器验证的同学要用 ln 以自然对数e为底,而不是一般的以10为底的 log)

我们还要在图上添加一条红线,标志正区域和负区域的边界。靠近红线的词可被归类为中性词。

fig, ax = plt.subplots(figsize = (8,8))

x = np.log([x[1] + 1 for x in data])

y = np.log([x[2] + 1 for x in data])

ax.scatter(x, y)

plt.xlabel("Log Positive count")
plt.ylabel("Log Negative count")

for i in range(0, len(data)):
    ax.annotate(data[i][0], (x[i], y[i]), fontsize = 12)

ax.plot([0,9], [0,9], color = 'red')
plt.show()

这张图很容易理解,特别是表情符号:(和:)在情感分析中似乎很重要。因此,在进行数据预处理时不能去掉这些符号。

此外我们还看到,皇冠所表示的意义似乎还蛮消极的。

感兴趣的同学建议可以试试把所有数据都放到图上来回发生什么。

ChangeLog

  • 2021/2/2 09:59:22 完成1-2步骤
  • 2021/2/2 17:11:14 完成剩余步骤及理解