一文概覽 NLP 算法 -Python-
一、自然語言處理(NLP)簡介
NLP,自然語言處理就是用計算機來分析和生成自然語言(文本、語音),目的是讓人類可以用自然語言形式跟計算機系統進行人機交互,從而更便捷、有效地進行信息管理。
NLP 是人工智能領域歷史較爲悠久的領域,但由於語言的複雜性(語言表達多樣性 / 歧義 / 模糊等等),如今的發展及收效相對緩慢。比爾 · 蓋茨曾說過,"NLP 是 AI 皇冠上的明珠。" 在光鮮絢麗的同時,卻可望而不可及(...)。
爲了揭開 NLP 的神祕面紗,本文接下來會梳理下 NLP 流程、主要任務及算法,並最終落到實際 NLP 項目(經典的文本分類任務的實戰)。順便說一句,個人水平有限,不足之處還請留言指出~~
二、NLP 主要任務及技術
NLP 任務可以大致分爲詞法分析、句法分析、語義分析三個層面。具體的,本文按照單詞 -》句子 -》文本做順序展開,並介紹各個層面的任務及對應技術。本節上半部分的分詞、命名實體識別、詞向量等等可以視爲 NLP 基礎的任務。下半部分的句子關係、文本生成及分類任務可以看做 NLP 主要的應用任務。
高清圖可如下路徑下載(原作者 graykode):https://github.com/aialgorithm/AiPy/tree/master/Ai%E7%9F%A5%E8%AF%86%E5%9B%BE%E5%86%8C/Ai_Roadmap
2.1 數據清洗 + 分詞(系列標註任務)
-
數據語料清洗。我們拿到文本的數據語料 (Corpus) 後,通常首先要做的是,分析並清洗下文本,主要用正則匹配刪除掉數字及標點符號(一般這些都是噪音,對於實際任務沒有幫助),做下分詞後,刪掉一些無關的詞(停用詞),對於英文還需要統一下複數、語態、時態等不同形態的單詞形式,也就是詞幹 / 詞形還原。
-
分詞。即劃分爲詞單元(token),是一個常見的序列標註任務。對於英文等拉丁語系的語句分詞,天然可以通過空格做分詞,
對於中文語句,由於中文詞語是連續的,可以用結巴分詞(基於 trie tree + 維特比等算法實現最大概率的詞語切分)等工具實現。
import jieba
jieba.lcut("我的地址是上海市松江區中山街道華光藥房")
>>> ['我', '的', '地址', '是', '上海市', '松江區', '中山', '街道', '華光', '藥房']
-
英文分詞後的詞幹 / 詞形等還原 (去除時態 語態及複數等信息,統一爲一個“單詞” 形態)。這並不是必須的,還是根據實際任務是否需要保留時態、語態等信息,有 WordNetLemmatizer、 SnowballStemmer 等方法。
-
分詞及清洗文本後,還需要對照前後的效果差異,在做些微調。這裏可以統計下個單詞的頻率、句長等指標,還可以通過像詞雲等工具做下可視化~
from wordcloud import WordCloud
ham_msg_cloud = WordCloud(width =520, height =260,max_font_size=50, background_color ="black", colormap='Blues').generate(原文本語料)
plt.figure(figsize=(16,10))
plt.imshow(ham_msg_cloud, interpolation='bilinear')
plt.axis('off') # turn off axis
plt.show()
2.2 詞性標註(系列標註任務)
詞性標註是對句子中的成分做簡單分析,區分出分名詞、動詞、形容詞之類。對於句法分析、信息抽取的任務,經過詞性標註後的文本會帶來很大的便利性(其他方面的應用好像比較少)。
常用的詞性標註有基於規則、統計以及深度學習的方法,像 HanLP、結巴分詞等工具都有這個功能。
2.3 命名實體識別(系列標註任務)
命名實體識別(Named Entity Recognition,簡稱 NER)是一個有監督的系列標註任務,又稱作 “專名識別”,是指識別文本中具有特定意義的實體,主要包括人名、地名、機構名、時間、專有名詞等關鍵信息。
2.4 詞向量(表示學習)
對於自然語言文本,計算機無法理解詞後面的含義。輸入模型前,首先要做的就是詞的數值化表示,常用的轉化方式有 2 種:One-hot 編碼、詞嵌入分佈式方法。
- One-hot 編碼:最簡單的表示方法某過於 onehot 表示,每個單詞是否出現就用一位數單獨展示。進一步,句子的表示也就是累加每個單詞的 onehot,也就是常說的句子的詞袋模型(bow)表示。
## 詞袋錶示
from sklearn.feature_extraction.text import CountVectorizer
bow = CountVectorizer(
analyzer = 'word',
strip_accents = 'ascii',
tokenizer = [],
lowercase = True,
max_features = 100,
)
- 詞嵌入分佈式表示:自然語言的單詞數是成千上萬的,One-hot 編碼會有高維、詞語間無聯繫的缺陷。這時有一種更有效的方法就是——詞嵌入分佈式表示,通過神經網絡學習構造一個低維、稠密,隱含詞語間關係的向量表示。常見有 Word2Vec、Fasttext、Bert 等模型學習每個單詞的向量表示,在表示學習後相似的詞彙在向量空間中是比較接近的。
# Fasttext embed模型
from gensim.models import FastText,word2vec
model = FastText(text, size=100,sg=1, window=3, min_count=1, iter=10, min_n=3, max_n=6,word_ngrams=1,workers=12)
print(model.wv['hello']) # 詞向量
model.save('./data/fasttext100dim')
特別地,正因爲 Bert 等大規模自監督預訓練方法,又爲 NLP 帶來了春天~
- 對於學習後的詞表示向量,還可以通過重要程度進行特徵加權,合適的加權方法對於任務可以有不錯的提升效果。常用的有卡方 chi2、TF-IDF 等加權方法。TF-IDF 是一種基於統計的方法,其核心思想是假設字詞的重要性與其在某篇文章中出現的比例成正比,與其在其他文章中出現的比例成反比。
# TF-IDF可以直接調用sklearn
from sklearn.feature_extraction.text import TfidfTransformer
2.5 句法、語義依存分析
句法、語義依存分析是傳統自然語言的基礎句子級的任務,語義依存分析是指在句子結構中分析實詞和實詞之間的語義關係,這種關係是一種事實上或邏輯上的關係,且只有當詞語進入到句子時纔會存在。語義依存分析的目的即回答句子的”Who did what to whom when and where” 的問題。例如句子 “張三昨天告訴李四一個祕密”,語義依存分析可以回答四個問題,即誰告訴了李四一個祕密,張三告訴誰一個祕密,張三什麼時候告訴李四一個祕密,張三告訴李四什麼。
傳統的自然語言處理多是參照了語言學家對於自然語言的歸納總結,通過句法、語義分析可以挖掘出詞語間的聯繫(主謂賓、施事受事等關係),用於制定文本規則、信息抽取(如正則匹配疊加語義規則應用於知識抽取或者構造特徵)。可以參考 spacy 庫、哈工大 NLP 的示例:http://ltp.ai/demo.html
隨着深度學習技術 RNN/LSTM 等強大的時序模型(sequential modeling)和詞嵌入方法的普及,能夠在一定程度上刻畫句子的隱含語法結構,學習到上下文信息,已經逐漸取代了詞法、句法等傳統自然語言處理流程。
2.6 相似度算法(句子關係的任務)
自然語言處理任務中,我們經常需要判斷兩篇文檔的相似程度(句子關係),比如檢索系統輸出最相關的文本,推薦系統推薦相似的文章。文本相似度匹配常用到的方法有:文本編輯距離、WMD、 BM2.5、詞向量相似度 、Approximate Nearest Neighbor 以及一些有監督的 (神經網絡) 模型判斷文本間相似度。
2.7 文本分類任務
文本分類是經典的 NLP 任務,就是將文本系列對應預測到類別。
-
一種是輸入序列輸出這整個序列的類別,如短信息、微博分類、意圖識別等。
-
另一種是輸入序列輸出序列上每個位置的類別,上文提及的系列標註可以看做爲詞粒度的一種分類任務,如實體命名識別。
分類任務使用預訓練 +(神經網絡) 分類模型的端對端學習是主流,深度學習學習特徵的表達然後進行分類,大大減少人工的特徵。但以實際項目中的經驗來看,對於一些困難任務(任務的噪聲大),加入些人工的特徵工程還是很有必要的。
2.8 文本生成任務
文本生成也就是由類別生成序列 或者 由序列到序列的預測任務。按照不同的輸入劃分,文本自動生成可包括文本到文本的生成 (text-to-text generation)、意義到文本的生成(meaning-to-text generation)、數據到文本的生成(data-to-text generation) 以及圖像到文本的生成 (image-to-text generation) 等。具體應用如機器翻譯、文本摘要理解、閱讀理解、閒聊對話、寫作、看圖說話。常用的模型如 RNN、CNN、seq2seq、Transformer。
同樣的,基於大規模預訓練模型的文本生成也是一大熱門,可見《A Survey of Pretrained Language Models Based Text Generation》
三、垃圾短信文本分類實戰
3.1 讀取短信文本數據並展示
本項目是通過有監督的短信文本,學習一個垃圾短信文本分類模型。數據樣本總的有 5572 條,label 有 spam(垃圾短信)和 ham 兩種,是一個典型類別不均衡的二分類問題。
# 源碼可見https://github.com/aialgorithm/Blog
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
spam_df = pd.read_csv('./data/spam.csv', header=0, encoding="ISO-8859-1")
# 數據展示
_, ax = plt.subplots(1,2,figsize=(10,5))
spam_df['label'].value_counts().plot(ax=ax[0], kind="bar", rot=90, title='label');
spam_df['label'].value_counts().plot(ax=ax[1], kind="pie", rot=90, title='label', ylabel='');
print("Dataset size: ", spam_df.shape)
spam_df.head(5)
3.2 數據清洗預處理
數據清洗在於去除一些噪聲信息,這裏對短信文本做按空格分詞,統一大小寫,清洗非英文字符,去掉停用詞並做了詞幹還原。考慮到短信文本里面的數字位數可能有一定的含義,這裏將數字替換爲‘x’的處理。最後,將標籤統一爲數值(0、1)是否垃圾短信。
# 導入相關的庫
import nltk
from nltk import word_tokenize
from nltk.corpus import stopwords
from nltk.data import load
from nltk.stem import SnowballStemmer
from string import punctuation
import re # 正則匹配
stop_words = set(stopwords.words('english'))
non_words = list(punctuation)
# 詞形、詞幹還原
# from nltk.stem import WordNetLemmatizer
# wnl = WordNetLemmatizer()
stemmer = SnowballStemmer('english')
def stem_tokens(tokens, stemmer):
stems = []
for token in tokens:
stems.append(stemmer.stem(token))
return stems
### 清除非英文詞彙並替換數值x
def clean_non_english_xdig(txt,isstem=True, gettok=True):
txt = re.sub('[0-9]', 'x', txt) # 去數字替換爲x
txt = txt.lower() # 統一小寫
txt = re.sub('[^a-zA-Z]', ' ', txt) #去除非英文字符並替換爲空格
word_tokens = word_tokenize(txt) # 分詞
if not isstem: #是否做詞幹還原
filtered_word = [w for w in word_tokens if not w in stop_words] # 刪除停用詞
else:
filtered_word = [stemmer.stem(w) for w in word_tokens if not w in stop_words] # 刪除停用詞及詞幹還原
if gettok: #返回爲字符串或分詞列表
return filtered_word
else:
return " ".join(filtered_word)
spam_df['token'] = spam_df.message.apply(lambda x:clean_non_english_xdig(x))
spam_df.head(3)
# 數據清洗
spam_df['token'] = spam_df.message.apply(lambda x:clean_non_english_xdig(x))
# 標籤整數編碼
spam_df['label'] = (spam_df.label=='spam').astype(int)
spam_df.head(3)
3.3 fasttext 詞向量表示學習
我們需要將單詞文本轉化爲數值的詞向量才能輸入模型。詞向量表示常用的詞袋、fasttext、bert 等方法,這裏訓練的是 fasttext,模型的主要輸入參數是,輸入分詞後的語料(通常訓練語料越多越好,當現有語料有限時候,直接拿 github 上合適的大規模預訓練模型來做詞向量也是不錯的選擇),詞向量的維度 size(一個經驗的詞向量維度設定是,dim > 8.33 logN, N 爲詞彙表的大小,當維度 dim 足夠大才能表達好這 N 規模的詞彙表的含義。可參考《# 最小熵原理(六):詞向量的維度應該怎麼選擇?By 蘇劍林》)。語料太大的時候可以使用 workers 開啓多進程訓練(其他參數及詞表示學習原理後續會專題介紹,也可以自行了解)。
# 訓練詞向量 Fasttext embed模型
from gensim.models import FastText,word2vec
fmodel = FastText(spam_df.token, size=100,sg=1, window=3, min_count=1, iter=10, min_n=3, max_n=6,word_ngrams=1,workers=12)
print(fmodel.wv['hello']) # 輸出hello的詞向量
# fmodel.save('./data/fasttext100dim')
按照句子所有的詞向量取平均,爲每一句子生成句向量。
fmodel = FastText.load('./data/fasttext100dim')
#對每個句子的所有詞向量取均值,來生成一個句子的vector
def build_sentence_vector(sentence,w2v_model,size=100):
sen_vec=np.zeros((size,))
count=0
for word in sentence:
try:
sen_vec+=w2v_model[word]#.reshape((1,size))
count+=1
except KeyError:
continue
if count!=0:
sen_vec/=count
return sen_vec
# 句向量
sents_vec = []
for sent in spam_df['token']:
sents_vec.append(build_sentence_vector(sent,fmodel,size=100))
print(len(sents_vec))
3.4 訓練文本分類模型
示例採用的 fasttext embedding + lightgbm 的二分類模型,類別不均衡使用 lgb 代價敏感學習解決(即 class_weight='balanced'),超參數是手動簡單配置的,可以自行搜索下較優超參數。
### 訓練文本分類模型
from sklearn.model_selection import train_test_split
from lightgbm import LGBMClassifier
from sklearn.linear_model import LogisticRegression
train_x, test_x, train_y, test_y = train_test_split(sents_vec, spam_df.label,test_size=0.2,shuffle=True,random_state=42)
result = []
clf = LGBMClassifier(class_weight='balanced',n_estimators=300, num_leaves=64, reg_alpha= 1,reg_lambda= 1,random_state=42)
#clf = LogisticRegression(class_weight='balanced',random_state=42)
clf.fit(train_x,train_y)
import pickle
# 保存模型
pickle.dump(clf, open('./saved_models/spam_clf.pkl', 'wb'))
# 加載模型
model = pickle.load(open('./saved_models/spam_clf.pkl', 'rb'))
3.5 模型評估
訓練集測試集按 0.2 劃分,分佈驗證訓練集測試集的 AUC、F1score 等指標,均有不錯的表現。
from sklearn.metrics import auc,roc_curve,f1_score,precision_score,recall_score
def model_metrics(model, x, y,tp='auc'):
""" 評估 """
yhat = model.predict(x)
yprob = model.predict_proba(x)[:,1]
fpr,tpr,_ = roc_curve(y, yprob,pos_label=1)
metrics = {'AUC':auc(fpr, tpr),'KS':max(tpr-fpr),
'f1':f1_score(y,yhat),'P':precision_score(y,yhat),'R':recall_score(y,yhat)}
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, 'k--', label='ROC (area = {0:.2f})'.format(roc_auc), lw=2)
plt.xlim([-0.05, 1.05]) # 設置x、y軸的上下限,以免和邊緣重合,更好的觀察圖像的整體
plt.ylim([-0.05, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate') # 可以使用中文,但需要導入一些庫即字體
plt.title('ROC Curve')
plt.legend(loc="lower right")
return metrics
print('train ',model_metrics(clf, train_x, train_y,tp='ks'))
print('test ',model_metrics(clf, test_x,test_y,tp='ks'))
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/7qWyJ-BtFl1yz72hbJ8K2w