はじめに
FastTextはMeta社(旧Facebook)によって開発されたオープンソースの自然言語処理ライブラリです。livedoorニュースコーパスの多クラス分類を行ったところ、非常にお手軽に実装でき、かなり良い精度が得られたので記事にしました。
開発環境はGoogle Colabを使用しました。この記事のコードを動作確認したGoogle Colabのノートブックは以下です。興味がある人は是非試してみて下さい。
ニュース記事の分類.ipynb
データの取得
- データセットはlivedoor ニュースコーパスを使用しました。こちらの記事を参考にさせていただき簡単にDataFrame化できました。
# Livedoorニュースのファイルをダウンロード&解凍
! wget "https://www.rondhuit.com/download/ldcc-20140209.tar.gz"
!tar -zxvf ldcc-20140209.tar.gz
# tsvファイルの作成
!echo -e "filename\tarticle"$(for category in $(basename -a `find ./text -type d` | grep -v text | sort); do echo -n "\t"; echo -n $category; done) > ./text/livedoor.tsv
!for filename in `basename -a ./text/dokujo-tsushin/dokujo-tsushin-*`; do echo -n "$filename"; echo -ne "\t"; echo -n `sed -e '1,3d' ./text/dokujo-tsushin/$filename`; echo -e "\t1\t0\t0\t0\t0\t0\t0\t0\t0"; done >> ./text/livedoor.tsv
!for filename in `basename -a ./text/it-life-hack/it-life-hack-*`; do echo -n "$filename"; echo -ne "\t"; echo -n `sed -e '1,3d' ./text/it-life-hack/$filename`; echo -e "\t0\t1\t0\t0\t0\t0\t0\t0\t0"; done >> ./text/livedoor.tsv
!for filename in `basename -a ./text/kaden-channel/kaden-channel-*`; do echo -n "$filename"; echo -ne "\t"; echo -n `sed -e '1,3d' ./text/kaden-channel/$filename`; echo -e "\t0\t0\t1\t0\t0\t0\t0\t0\t0"; done >> ./text/livedoor.tsv
!for filename in `basename -a ./text/livedoor-homme/livedoor-homme-*`; do echo -n "$filename"; echo -ne "\t"; echo -n `sed -e '1,3d' ./text/livedoor-homme/$filename`; echo -e "\t0\t0\t0\t1\t0\t0\t0\t0\t0"; done >> ./text/livedoor.tsv
!for filename in `basename -a ./text/movie-enter/movie-enter-*`; do echo -n "$filename"; echo -ne "\t"; echo -n `sed -e '1,3d' ./text/movie-enter/$filename`; echo -e "\t0\t0\t0\t0\t1\t0\t0\t0\t0"; done >> ./text/livedoor.tsv
!for filename in `basename -a ./text/peachy/peachy-*`; do echo -n "$filename"; echo -ne "\t"; echo -n `sed -e '1,3d' ./text/peachy/$filename`; echo -e "\t0\t0\t0\t0\t0\t1\t0\t0\t0"; done >> ./text/livedoor.tsv
!for filename in `basename -a ./text/smax/smax-*`; do echo -n "$filename"; echo -ne "\t"; echo -n `sed -e '1,3d' ./text/smax/$filename`; echo -e "\t0\t0\t0\t0\t0\t0\t1\t0\t0"; done >> ./text/livedoor.tsv
!for filename in `basename -a ./text/sports-watch/sports-watch-*`; do echo -n "$filename"; echo -ne "\t"; echo -n `sed -e '1,3d' ./text/sports-watch/$filename`; echo -e "\t0\t0\t0\t0\t0\t0\t0\t1\t0"; done >> ./text/livedoor.tsv
!for filename in `basename -a ./text/topic-news/topic-news-*`; do echo -n "$filename"; echo -ne "\t"; echo -n `sed -e '1,3d' ./text/topic-news/$filename`; echo -e "\t0\t0\t0\t0\t0\t0\t0\t0\t1"; done >> ./text/livedoor.tsv
- 作成したtsvファイルをpandasで読み込みます。下記画像のように、クリーニングした記事の内容とニュースカテゴリのone-hot表現を持ったDataFrameが作成できました。
import pandas as pd
df = pd.read_csv('text/livedoor.tsv', sep='\t')
クリーニング
- 形態素解析器としてMeCabを用います。以下記事を参考にクリーニングクラスを定義しています。
Mecab+neologd辞書を用いる前に推奨される正規化
import re, unicodedata
class CleaningData:
def __init__(self, df, target_column):
self.df = df
self.target_column = target_column
def cleaning(self):
self.df[self.target_column] = self.df[self.target_column].map(self.remove_extra_spaces)
self.df[self.target_column] = self.df[self.target_column].map(self.normalize_neologd)
# クリーニングの過程でtextが空になった行を削除
self.df = self.df[self.df[self.target_column] != '']
self.df = self.df[self.df[self.target_column] != '']
self.df = self.df.reset_index()
return self.df
def unicode_normalize(self, cls, s):
pt = re.compile('([{}]+)'.format(cls))
def norm(c):
return unicodedata.normalize('NFKC', c) if pt.match(c) else c
s = ''.join(norm(x) for x in re.split(pt, s))
s = re.sub('-', '-', s)
return s
def remove_extra_spaces(self, s):
s = re.sub('[ ]+', ' ', s)
blocks = ''.join(('\u4E00-\u9FFF', # CJK UNIFIED IDEOGRAPHS
'\u3040-\u309F', # HIRAGANA
'\u30A0-\u30FF', # KATAKANA
'\u3000-\u303F', # CJK SYMBOLS AND PUNCTUATION
'\uFF00-\uFFEF' # HALFWIDTH AND FULLWIDTH FORMS
))
basic_latin = '\u0000-\u007F'
def remove_space_between(cls1, cls2, s):
p = re.compile('([{}]) ([{}])'.format(cls1, cls2))
while p.search(s):
s = p.sub(r'\1\2', s)
return s
s = remove_space_between(blocks, blocks, s)
s = remove_space_between(blocks, basic_latin, s)
s = remove_space_between(basic_latin, blocks, s)
return s
def normalize_neologd(self, s):
s = s.strip()
s = self.unicode_normalize('0-9A-Za-z。-゚', s)
def maketrans(f, t):
return {ord(x): ord(y) for x, y in zip(f, t)}
s = re.sub('[˗֊‐‑‒–⁃⁻₋−]+', '-', s) # normalize hyphens
s = re.sub('[﹣-ー—―─━ー]+', 'ー', s) # normalize choonpus
s = re.sub('[~∼∾〜〰~]', '', s) # remove tildes
s = s.translate(
maketrans('!"#$%&\'()*+,-./:;<=>?@[¥]^_`{|}~。、・「」',
'!”#$%&’()*+,-./:;<=>?@[¥]^_`{|}〜。、・「」'))
s = self.remove_extra_spaces(s)
s = self.unicode_normalize('!”#$%&’()*+,-./:;<>?@[¥]^_`{|}〜', s) # keep =,・,「,」
s = re.sub('[’]', '\'', s)
s = re.sub('[”]', '"', s)
return s
def remove_symbols(self, text):
text = re.sub(r'[◎, 〇, △, ▲, ×, ◇, □]', '', text)
return text
- クリーニングの実行
cd = CleaningData(df, 'article')
df = cd.cleaning()
形態素解析
- 形態素分析ライブラリーMeCab と 辞書(mecab-ipadic-NEologd)のインストール
!apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n
!pip install mecab-python3
!ln -s /etc/mecabrc /usr/local/etc/mecabrc
- 形態素解析クラスの作成。今回は学習に名詞・形容詞・動詞のみ用います。
import MeCab
class Wakati:
""" 形態素解析クラス """
# クラス変数
MECAB_PATH = "-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd"
def __init__(self, df, target_column):
self.df = df
self.target_column = target_column
def wakati_document(self):
self.df[self.target_column] = self.df[self.target_column].map(self.wakati_sentence)
return self.df
def wakati_sentence(self, text):
tagger = MeCab.Tagger(Wakati.MECAB_PATH)
words = []
for c in tagger.parse(text).splitlines()[:-1]:
#surfaceに単語、featureに解析結果が入る
try:
surface, feature = c.split('\t')
except:
continue
pos = feature.split(',')[0]
surface = feature.split(',')[6] # 原型に直す
if pos in ['名詞','形容詞','動詞']:
words.append(surface)
else:
continue
return ' '.join(words)
- 形態素解析の実行
w = Wakati(df, 'article')
df = w.wakati_document()
正解ラベルの付与
- Fasttextの学習用のテキストファイルを作成します。
- テキストファイルの各行には、ラベルのリストと、対応する文書を含みます。すべてのラベルは
__label__という接頭辞で始まります。
label_dict = {
'dokujo-tsushin': 0,
'it-life-hack': 1,
'kaden-channel': 2,
'livedoor-homme': 3,
'movie-enter': 4,
'peachy': 5,
'smax': 6,
'sports-watch': 7,
'topic-news': 8}
for category, index in label_dict.items():
df.loc[df[category]==1, 'article'] = f'__label__{index} ' + df.loc[df[category]==1, 'article']
データセットの分割
- scikit-learnのtrain_test_split()を用いて訓練データ、テストデータ、検証データに分割します。
from sklearn.model_selection import train_test_split
X_train, X_test = train_test_split(df['article'], test_size=0.2, random_state=100)
X_valid, X_test = train_test_split(X_test, test_size=0.5, random_state=100)
- 作成したデータセットをテキストファイルに書き込みます。ここまでで学習の準備は完了です。
with open('drive/My Drive/Colab Notebooks/livedoor_fasttext_train.txt', 'w') as temp_file:
for text in X_train:
temp_file.write(f"{text}\n")
with open('drive/My Drive/Colab Notebooks/livedoor_fasttext_valid.txt', 'w') as temp_file:
for text in X_valid:
temp_file.write(f"{text}\n")
with open('drive/My Drive/Colab Notebooks/livedoor_fasttext_test.txt', 'w') as temp_file:
for text in X_test:
temp_file.write(f"{text}\n")
学習
- FastTextライブラリのインストール
!pip install fasttext
- 訓練データと検証データを渡すと、よしなに学習してくれます。各パラメータ等詳しく知りたい方は公式を参照してみてください。今回はautotuneDurationパラメータを600[s]に設定しているので、学習は10分で終わります。
import fasttext
model = fasttext.train_supervised(input='drive/My Drive/Colab Notebooks/livedoor_fasttext_train.txt', autotuneValidationFile='drive/My Drive/Colab Notebooks/livedoor_fasttext_valid.txt',autotuneDuration=600,epoch=20,wordNgrams=2)
結果
- 実際にテストデータを与えて精度を確認した結果がこちら
ret = model.test('drive/My Drive/Colab Notebooks/livedoor_fasttext_test.txt')
print(ret)
> (737, 0.9430122116689281, 0.9430122116689281)
なんと正解率94.3%で9クラス分類ができました。FastText恐るべし、、、
最後に、t-SNEを用いて可視化してみました
!pip install japanize_matplotlib
%matplotlib inline
import matplotlib.pyplot as plt
import japanize_matplotlib, re
import seaborn as sns
import pandas as pd
from sklearn.manifold import TSNE
sns.set(font="IPAexGothic")
def t_SNE(df, label_dict):
df['category'] = df['article'].map(lambda x: re.sub(r'\D', '', x.split(' ')[0]))
df['article'] = df['article'].map(lambda x: ' '.join(x.split(' ')[1:]))
df = df[['category','article']]
# 分散表現の獲得
vectors = []
for split_word in df['article']:
vectors.append(model.get_sentence_vector(split_word))
#t-SNEで次元削減
tsne = TSNE(n_components=2, random_state = 0, perplexity = 30, n_iter = 1000)
vectors_embedded = tsne.fit_transform(vectors)
ddf = pd.concat([df, pd.DataFrame(vectors_embedded, columns = ['col1', 'col2'])], axis = 1)
label_list = list(label_dict.keys())
colors = ["r", "g", "b", "c", "m", "y", "k", "orange","pink"]
plt.figure(figsize = (30, 30))
for i , v in enumerate(label_list):
tmp_df = ddf[ddf["category"] == str(i)]
plt.scatter(tmp_df['col1'],
tmp_df['col2'],
label = v,
color = colors[i])
plt.legend(fontsize = 30)
plt.savefig('livedoor_classification.png')
t_SNE(df, label_dict)
まとめ
今回、ニュース記事の9クラス分類を行ったところ、正解率は94.3%でした。基本的な前処理やクリーニングを加えただけでここまでの精度が出るのは凄いです(笑)
今後、他のモデルについてもまとめられたいいなと思っています。
最後まで読んでいただきありがとうございました!