假設有一段文本:"I have a cat, his name is Huzihu. Huzihu is really cute and friendly. We are good friends." 那麼怎麼提取這段文本的特征呢?
一個簡單的方法就是使用詞袋模型(bag of words model)。標明文本内一定的詞放入詞袋,統計詞袋内所有詞在文本中出現的次數(忽略文法和單詞出現的順序),将其用向量的形式表示出來。
詞頻統計可以用scikit-learn的CountVectorizer實作:
text1="I have a cat, his name is Huzihu. Huzihu is really cute and friendly. We are good friends."
from sklearn.feature_extraction.text import CountVectorizer
CV=CountVectorizer()
words=CV.fit_transform([text1]) #這裡注意要把文本字元串變為清單進行輸入
print(words)
首先CountVectorizer将文本映射成字典,字典的鍵是文本内的詞,值是詞的索引,然後對字典進行學習,将其轉換成詞頻矩陣并輸出:
(0, 3) 1
(0, 4) 1
(0, 0) 1
(0, 11) 1
(0, 2) 1
(0, 10) 1
(0, 7) 2
(0, 8) 2
(0, 9) 1
(0, 6) 1
(0, 1) 1
(0, 5) 1
(0, 7) 2 代表第7個詞"Huzihu"出現了2次。
注:CountVectorizer類會把文本全部轉換成小寫,然後将文本詞塊化(tokenize)。文本詞塊化是把句子分割成詞塊(token)或有意義的字母序列的過程。詞塊大多是單詞,但它們也可能是一些短語,如标點符号和詞綴。CountVectorizer類通過正規表達式用空格分割句子,然後抽取長度大于等于2的字母序列。(摘自:http://lib.csdn.net/article/machinelearning/42813)
我們一般提取文本特征是用于文檔分類,那麼就需要知道各個文檔之間的相似程度。可以通過計算文檔特征向量之間的歐氏距離(Euclidean distance)來進行比較。
讓我們添加另外兩段文本,看看這三段文本之間的相似程度如何。
文本二:"My cousin has a cute dog. He likes sleeping and eating. He is friendly to others."
文本三:"We all need to make plans for the future, otherwise we will regret when we're old."
text1="I have a cat, his name is Huzihu. Huzihu is really cute and friendly. We are good friends."
text2="My cousin has a cute dog. He likes sleeping and eating. He is friendly to others."
text3= "We all need to make plans for the future, otherwise we will regret when we're old."
corpus=[text1,text2,text3] #把三個文檔放入語料庫
from sklearn.feature_extraction.text import CountVectorizer
CV=CountVectorizer()
words=CV.fit_transform(corpus)
words_frequency=words.todense() #用todense()轉化成矩陣
print(CV.get_feature_names())
print(words_frequency)
此時分别輸出的是特征名稱和由每個文本的詞頻向量組成的矩陣:
['all', 'and', 'are', 'cat', 'cousin', 'cute', 'dog', 'eating', 'for', 'friendly', 'friends', 'future', 'good', 'has', 'have', 'he', 'his', 'huzihu', 'is', 'likes', 'make', 'my', 'name', 'need', 'old', 'others', 'otherwise', 'plans', 're', 'really', 'regret', 'sleeping', 'the', 'to', 'we', 'when', 'will']
[[0 1 1 ..., 1 0 0]
[0 1 0 ..., 0 0 0]
[1 0 0 ..., 3 1 1]]
可以看到,矩陣第一列,其中前兩個數都為0,最後一個數為1,代表"all"在前兩個文本中都未出現過,而在第三個文本中出現了一次。
接下來,我們就可以用sklearn中的euclidean_distances來計算這三個文本特征向量之間的距離了。
from sklearn.metrics.pairwise import euclidean_distances
for i,j in ([0,1],[0,2],[1,2]):
dist=euclidean_distances(words_frequency[i],words_frequency[j])
print("文本{}和文本{}特征向量之間的歐氏距離是:{}".format(i+1,j+1,dist))
輸出如下:
文本1和文本2特征向量之間的歐氏距離是:[[ 5.19615242]]
文本1和文本3特征向量之間的歐氏距離是:[[ 6.08276253]]
文本2和文本3特征向量之間的歐氏距離是:[[ 6.164414]]
可以看到,文本一和文本二之間最相似。
現在思考一下,應該選什麼樣的詞放入詞袋呢?有一些詞并不能提供多少有用的資訊,比如:the, be, you, he...這些詞被稱為停止詞(stop words)。由于文本内包含的詞的數量非常之多(詞袋内的每一個詞都是一個次元),是以我們需要盡量減少次元,去除這些噪音,以便更好地計算和拟合。
可以在建立CountVectorizer執行個體時添加stop_words="english"參數來去除這些停用詞。
另外,也可以下載下傳NLTK(Natural Language Toolkit)自然語言工具包,使用其裡面的停用詞。
下面,我們就用NLTK來試一試(使用之前,請大家先下載下傳安裝:pip install NLTK):
text1="I have a cat, his name is Huzihu. Huzihu is really cute and friendly. We are good friends."
text2="My cousin has a cute dog. He likes sleeping and eating. He is friendly to others."
text3= "We all need to make plans for the future, otherwise we will regret when we're old."
corpus=[text1,text2,text3]
from nltk.corpus import stopwords
noise=stopwords.words("english")
from sklearn.feature_extraction.text import CountVectorizer
CV=CountVectorizer(stop_words=noise)
words=CV.fit_transform(corpus)
words_frequency=words.todense()
print(CV.get_feature_names())
print(words_frequency)
輸出:
['cat', 'cousin', 'cute', 'dog', 'eating', 'friendly', 'friends', 'future', 'good', 'huzihu', 'likes', 'make', 'name', 'need', 'old', 'others', 'otherwise', 'plans', 'really', 'regret', 'sleeping']
[[1 0 1 ..., 1 0 0]
[0 1 1 ..., 0 0 1]
[0 0 0 ..., 0 1 0]]
可以看到,此時詞袋裡的詞減少了。通過檢視words_frequncy.shape,我們發現特征向量的次元也由原來的37變為了21。
還有一個需要考慮的情況,比如說文本中出現的friendly和friends意思相近,可以看成是一個詞。但是由于之前把這兩個詞分别算成是兩個不同的特征,這就可能導緻文本分類出現偏差。解決辦法是對單詞進行詞幹提取(stemming),再把詞幹放入詞袋。
下面用NLTK中的SnowballStemmer來提取詞幹(注意:需要先用正規表達式把文本中的詞提取出來,也就是進行詞塊化,再提取詞幹,是以在用CountVectorizer時可以把tokenizer參數設為自己寫的function):
text1="I have a cat, his name is Huzihu. Huzihu is really cute and friendly. We are good friends."
text2="My cousin has a cute dog. He likes sleeping and eating. He is friendly to others."
text3= "We all need to make plans for the future, otherwise we will regret when we're old."
corpus=[text1,text2,text3]
from nltk import RegexpTokenizer
from nltk.stem.snowball import SnowballStemmer
def stemming(token):
stemming=SnowballStemmer("english")
stemmed=[stemming.stem(each) for each in token]
return stemmed
def tokenize(text):
tokenizer=RegexpTokenizer(r'\w+') #設定正規表達式規則
tokens=tokenizer.tokenize(text)
stems=stemming(tokens)
return stems
from nltk.corpus import stopwords
noise=stopwords.words("english")
from sklearn.feature_extraction.text import CountVectorizer
CV=CountVectorizer(stop_words=noise,tokenizer=tokenize,lowercase=False)
words=CV.fit_transform(corpus)
words_frequency=words.todense()
print(CV.get_feature_names())
print(words_frequency)
['cat', 'cousin', 'cute', 'dog', 'eat', 'friend', 'futur', 'good', 'huzihu', 'like', 'make', 'name', 'need', 'old', 'otherwis', 'plan', 'realli', 'regret', 'sleep']
[[1 0 1 ..., 1 0 0]
[0 1 1 ..., 0 0 1]
[0 0 0 ..., 0 1 0]]
可以看到,friendly和friends在提取詞幹後都變成了friend。而others提取詞幹後變為other,other屬于停用詞,被移除了,是以現在詞袋特征向量次元變成了19。
此外,還需注意的是詞形的變化。比如說單複數:"foot"和"feet",過去式和現在進行時:"understood"和"understanding",主動和被動:"eat"和"eaten",等等。這些詞都應該被視為同一個特征。解決的辦法是進行詞形還原(lemmatization)。這裡就不示範了,可以用NLTK中的WordNetLemmatizer來進行詞形還原(from nltk.stem.wordnet import WordNetLemmatizer)。
詞幹提取和詞形還原的差別可參見:https://www.neilx.com/blog/?p=1425。
最後,再想一下,長文本和短文本包含的資訊是不對等的,一般來說,長文本包含的關鍵詞要比短文本多,是以,我們需要對文本進行歸一化處理,将每個單詞出現的次數除以該文本中所有單詞的個數,這被稱之為詞頻(term frequency)(注:之前說的詞頻是指絕對頻率,這裡的詞頻是指相對頻率)。其次,我們在對文檔進行分類時,假如某個詞在各文本中都有出現,那麼這個詞就無法給分類帶來多少有用的資訊。是以,對于出現頻率高的詞和頻率低的詞,我們應該區分對待,它們的重要性是不一樣的。解決的辦法就是用逆文檔頻率(inverse document frequency)來給詞進行權重。IDF會根據單詞在文本中出現的頻率進行權重,出現頻率高的詞,權重系數就低,反之,出現頻率低的詞,權重系數就高。這兩者相結合被稱之為TF-IDF(term frequncy, inverse document frequency)。可以用sklearn的TfidfVectorizer來實作。
下面,我們把CountVectorizer換成TfidfVectorizer(包括之前使用過的提取詞幹和去除停用詞),再來計算一下這三個文本之間的相似度:
text1="I have a cat, his name is Huzihu. Huzihu is really cute and friendly. We are good friends."
text2="My cousin has a cute dog. He likes sleeping and eating. He is friendly to others."
text3= "We all need to make plans for the future, otherwise we will regret when we're old."
corpus=[text1,text2,text3]
from nltk import RegexpTokenizer
from nltk.stem.snowball import SnowballStemmer
def stemming(token):
stemming=SnowballStemmer("english")
stemmed=[stemming.stem(each) for each in token]
return stemmed
def tokenize(text):
tokenizer=RegexpTokenizer(r'\w+') #設定正規表達式規則
tokens=tokenizer.tokenize(text)
stems=stemming(tokens)
return stems
from nltk.corpus import stopwords
noise=stopwords.words("english")
from sklearn.feature_extraction.text import TfidfVectorizer
CV=TfidfVectorizer(stop_words=noise,tokenizer=tokenize,lowercase=False)
words=CV.fit_transform(corpus)
words_frequency=words.todense()
print(CV.get_feature_names())
print(words_frequency)
from sklearn.metrics.pairwise import euclidean_distances
for i,j in ([0,1],[0,2],[1,2]):
dist=euclidean_distances(words_frequency[i],words_frequency[j])
print("文本{}和文本{}特征向量之間的歐氏距離是:{}".format(i+1,j+1,dist))
['cat', 'cousin', 'cute', 'dog', 'eat', 'friend', 'futur', 'good', 'huzihu', 'like', 'make', 'name', 'need', 'old', 'otherwis', 'plan', 'realli', 'regret', 'sleep']
[[ 0.30300252 0. 0.23044123 ..., 0.30300252 0. 0. ]
[ 0. 0.40301621 0.30650422 ..., 0. 0. 0.40301621]
[ 0. 0. 0. ..., 0. 0.37796447 0. ]]
文本1和文本2特征向量之間的歐氏距離是:[[ 1.25547312]]
文本1和文本3特征向量之間的歐氏距離是:[[ 1.41421356]]
文本2和文本3特征向量之間的歐氏距離是:[[ 1.41421356]]
可以看到,現在特征值不再是單詞出現的次數了,而是相對頻率權重之後的值。雖然我們隻用了很短的文本進行測試,但還是能看出來,經過一系列優化後,計算出的結果更準确了。
詞袋模型的缺點: 1. 無法反映詞之間的關聯關系。例如:"Humans like cats."和"Cats like humans"具有相同的特征向量。
2. 無法捕捉否定關系。例如:"I will not eat noodles today."和"I will eat noodles today."盡管意思相反,但是從特征向量來看它們非常相似。
不過這些問題有一部分可以通過使用N-gram模型來解決(可以在用sklearn建立CountVectorizer執行個體時加上ngram_range參數)。