10
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FastTextを使ってサクッと多クラス分類

Last updated at Posted at 2022-02-19

はじめに

FastTextはMeta社(旧Facebook)によって開発されたオープンソースの自然言語処理ライブラリです。livedoorニュースコーパスの多クラス分類を行ったところ、非常にお手軽に実装でき、かなり良い精度が得られたので記事にしました。

開発環境はGoogle Colabを使用しました。この記事のコードを動作確認したGoogle Colabのノートブックは以下です。興味がある人は是非試してみて下さい。
ニュース記事の分類.ipynb

データの取得

# 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')

image.png

クリーニング

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']
  • 記事本文の先頭にラベルを振ることができました。
    image.png

データセットの分割

  • 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)
  • t-SNEで可視化した結果がこちら。すごく綺麗に分類できています!
    Untitled (1).png

まとめ

今回、ニュース記事の9クラス分類を行ったところ、正解率は94.3%でした。基本的な前処理やクリーニングを加えただけでここまでの精度が出るのは凄いです(笑)

今後、他のモデルについてもまとめられたいいなと思っています。
最後まで読んでいただきありがとうございました!

参考記事

10
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?