🤖

【自然言語処理】Python初心者でも文章要約タスクが実装できた

2022/08/24に公開

はじめに

わたくし、Python歴1年の初心者ですが、このたび、苦労に苦労を重ねて、自然言語処理タスクの文章要約を実装できました。
自然言語処理に興味のあるPython初心者のお役に立てればと、記事に残したいと思います。
 
実装にあたっては、ネットの記事も手あたり次第調べましたが、最終的には、以下の本が大変参考になりました。
ただし、バージョンの変更により、この本の通りに実装しても、2022年8月時点ではエラーになる箇所があります。出版社経由で著者の方にお聞きして一部コードを修正したほか、自分なりに工夫をして実装しました
https://www.borndigital.co.jp/book/23409.html
 

モデルについて

Huggingface社が提供している深層学習フレームワークのTransformersを使います。
transformersにはBERTをはじめとするさまざまな言語モデルが実装されていますが、今回のタスクでは、T5というモデルをファインチューニングして使います。
T5の正式名称はText to Text Transfer Tranformerで頭にTが5あるのでT5と呼ばれています。文章要約には抽出型と抽象型の2種類あって、簡単に言うと、抽出型は重要な文章を抜き出す、抽象型は文章を作りにいくものです。T5は抽象型にあたります。

 

準備

githubからコードをダウンロードしておきます。これは、学習用データをスクレイピングする際に必要になります。
あわせて、作業用にworkディレクトリを作成しておきます。

!git clone https://github.com/KodairaTomonori/ThreeLineSummaryDataset.git
!mkdir work

 
 

学習用データの収集

ファインチューニングをするにあたって、学習用データを集めます
日本語の文章要約モデルを実装するにあたって、一番、苦労するのがこの学習用データ集めだと思います。なにせ、要約データが世の中にほとんどない!!
ここではLivedoorニュースの記事本文とサマリーデータを学習用データとして使います。
記事本文とサマリーをスクレイピングでとってくる作業となります。

from urllib.request import urlopen
from bs4 import BeautifulSoup
from bs4.element import NavigableString
from pprint import pprint
import time

# 収集するニュース記事のインデックス

start_index = 0 # 開始インデックス
end_index = 5000 # 終了インデックス←ここに取得したい件数を指定します。最初は10件でテストします。

# コンテンツの取得
def get_content(id):
    # サーバに負荷をかけないように10秒スリープ
    time.sleep(10)

    # URL
    URL = 'https://news.livedoor.com/article/detail/'+id+'/'
    print(URL)
    try:
        with urlopen(URL) as res:
            # 本文の抽出
            output1 = ''
            html = res.read().decode('euc_jp', 'ignore')
            soup = BeautifulSoup(html, 'html.parser')
            lineList = soup.select('.articleBody p')
            for line in lineList:
                if len(line.contents) > 0 and type(line.contents[0]) == NavigableString:
                    output1 += line.contents[0].strip()
            if output1 == '': # 記事がない
                return
            output1 += '\n'

            # 要約の抽出
            output0 = ''
            summaryList = soup.select('.summaryList li')
            for summary in summaryList:
                output0 += summary.contents[0].strip()+'\t'
            if output0 == '': # 記事がない
                return

            # 出力
            print(output0+output1)
            with open('/content/output.tsv', mode='a') as f:
                f.writelines(output0+output1)
    except Exception:
        print('Exception')

# IDリストの生成の取得
idList = []
with open('/content/ThreeLineSummaryDataset/data/train.csv', mode='r') as f:
    lines = f.readlines()
    for line in lines:
        id = line.strip().split(',')[3].split('.')[0]
        idList.append(id)

# コンテンツの取得
for i in range(start_index, end_index):
    print('index:', i)
    get_content(idList[i])

 
 

データセットの作成

tsvファイルを開いてみましょう。

import pandas as pd
df = pd.read_csv("/content/output.tsv",sep="\t")
df.head()

以下のようになっていると思います。
少し見にくいですが、0列目〜2列目までがサマリー、3列目が本文という構成です。
サマリーが3列に分かれているのは、3行サマリーだからです。

 

学習用データ・訓練用データの作成

まず、さきほど集めたtsvファイルを、本文(説明変数)とサマリー(目的変数)に分けます。

学習用データと訓練用データを8:2で分割し、最初に作ったworkディレクトリに格納します。

import os
import pandas as pd

df = pd.DataFrame(columns=["text","summary"])

with open ("/content/output.tsv") as f:
    for line in f.readlines():
        strs = line.split("\t")
        summary = strs[0]+"。"+strs[1]+"。"+strs[2]
        df = df.append({"text":strs[3], "summary":summary},ignore_index=True)

df = df.sample(frac=1)

num = len(df)
df[:int(num*0.8)].to_csv("/content/work/train.csv",sep="," ,index=False)
df[int(num*0.8):].to_csv("/content/work/dev.csv",sep=",",index=False)

workディレクトリに移動します。

!cd /content/work

Transformersのインストール

本では2行目のtransformersの前に -eがついていますが、-eは削除しましょう。

!git clone https://github.com/huggingface/transformers -b v4.4.2
!pip install transformers
!pip install fugashi[unidic-lite]
!pip install ipadic

ランタイムを再起動
「ランタイム」→「ランタイムを再起動」を押します。ふたたび、workディレクトリに移動します。

Huggingface Datasetsのインストール

dill==0.3.3をインストールしないとエラーになります。

!pip install datasets==1.2.1
# 依存パッケージのインストール
!pip install rouge_score==0.0.4
!pip install sentencepiece==0.1.91
#dill=0.3.3にしないとエラーが出る
!pip install dill==0.3.3

ファインチューニングの実行

日本語T5事前学習済みモデルを使用します。これはWikipediaの日本語ダンプデータを使用して事前学習したものです。
この事前学習済みモデルにlivedoorニュースの本文とサマリーを与えてファインチューニングを行います。
Transformersのファインチューニングのサンプルコードは、work/transformers/examples/seq2seq/run_summarization.pyにあります。以下のコマンドを
GoogleColaboratoryに入力して、ファインチューニングを実行します。(しばらく時間がかかります)

%%time

!python ./transformers/examples/seq2seq/run_summarization.py \
    --model_name_or_path=sonoisa/t5-base-japanese \
    --do_train \
    --do_eval \
    --train_file=train.csv \
    --validation_file=dev.csv \
    --num_train_epochs=10 \
    --per_device_train_batch_size=4 \
    --per_device_eval_batch_size=4 \
    --save_steps=5000 \
    --save_total_limit=3 \
    --output_dir=output/ \
    --predict_with_generate \
    --use_fast_tokenizer=False \
    --logging_steps=100

実行結果で以下のような記述が出力され、workディレクトリ下のoutputフォルダにモデルのファイル群が入っていれば成功です。

***** train metrics *****
  epoch                    =       10.0
  train_loss               =     1.9895
  train_runtime            = 0:40:50.61
  train_samples            =       2073
  train_samples_per_second =      8.459
  train_steps_per_second   =      2.118
08/24/2022 02:12:47 - INFO - __main__ -   *** Evaluate ***
***** Running Evaluation *****
  Num examples = 519
  Batch size = 4
100% 130/130 [04:16<00:00,  1.97s/it]
***** eval metrics *****
  epoch                   =       10.0
  eval_gen_len            =    58.1811
  eval_loss               =     2.0293
  eval_rouge1             =    22.6572
  eval_rouge2             =     4.7625
  eval_rougeL             =    22.0124
  eval_rougeLsum          =    22.0554
  eval_runtime            = 0:04:18.24
  eval_samples            =        519
  eval_samples_per_second =       2.01
  eval_steps_per_second   =      0.503
CPU times: user 25.1 s, sys: 3.07 s, total: 28.2 s
Wall time: 45min 48s

 

要約の実行

江戸川乱歩の「怪人二十面相」の冒頭部分を要約してみたいと思います。

import torch
from transformers import pipeline, AutoTokenizer, AutoModelForSeq2SeqLM

# トークナイザーとモデルの準備
tokenizer = AutoTokenizer.from_pretrained('sonoisa/t5-base-japanese') 
model = AutoModelForSeq2SeqLM.from_pretrained('output/')    

# 要約したい文章
text="そのころ、東京中の町という町、家という家では、ふたり以上の人が顔をあわせさえすれば、まるでお天気のあいさつでもするように、怪人「二十面相」のうわさをしていました。「二十面相」というのは、毎日毎日、新聞記事をにぎわしている、ふしぎな盗賊のあだ名です。その賊は二十のまったくちがった顔を持っているといわれていました。つまり、変装がとびきりじょうずなのです。 どんなに明るい場所で、どんなに近よってながめても、少しも変装とはわからない、まるでちがった人に見えるのだそうです。老人にも若者にも、富豪にも乞食にも、学者にも無頼漢にも、いや、女にさえも、まったくその人になりきってしまうことができるといいます。では、その賊のほんとうの年はいくつで、どんな顔をしているのかというと、それは、だれひとり見たことがありません。二十種もの顔を持っているけれど、そのうちの、どれがほんとうの顔なのだか、だれも知らない。いや、賊自身でも、ほんとうの顔をわすれてしまっているのかもしれません。それほど、たえずちがった顔、ちがった姿で、人の前にあらわれるのです。そういう変装の天才みたいな賊だものですから、警察でもこまってしまいました。いったい、どの顔を目あてに捜索したらいいのか、まるで見当がつかないからです。ただ、せめてものしあわせは、この盗賊は、宝石だとか、美術品だとか、美しくてめずらしくて、ひじょうに高価な品物をぬすむばかりで、現金にはあまり興味を持たないようですし、それに、人を傷つけたり殺したりする、ざんこくなふるまいは、一度もしたことがありません。血がきらいなのです。しかし、いくら血がきらいだからといって、悪いことをするやつのことですから、自分の身があぶないとなれば、それをのがれるためには、何をするかわかったものではありません。東京中の人が「二十面相」のうわさばかりしているというのも、じつは、こわくてしかたがないからです。ことに、日本にいくつという貴重な品物を持っている富豪などは、ふるえあがってこわがっていました。今までのようすで見ますと、いくら警察へたのんでも、ふせぎようのない、おそろしい賊なのですから。"

# テキストをテンソルに変換
input = tokenizer.encode(text, return_tensors='pt', max_length=512, truncation=True)

# 推論
model.eval()
with torch.no_grad():
    summary_ids = model.generate(input)
    print(tokenizer.decode(summary_ids[0]))

結果は次の通りです。きちんと要約できています。
他の文章でも試してみましたが、100点でありませんが、割と良い感じに要約できます。

<pad> ふしぎな盗賊「二十面相」の変装術を紹介している。二十のまったくちがった顔を持っている盗賊で、警察でもこまってしまったという。人を傷つけたり殺したりする、ざんこくなふるまいは一度もしたことがないという</s>

 

さいごに

いかがでしたでしょうか。
最初、少ない学習用データ(500件ぐらい)で、かつ、1行サマリーでファインチューニングさせていましたが、要約の精度はよくありませんでした。そこで、データの件数を2,500に増やし、3行サマリーでファインチューニングしたところ、かなり精度が改善しました。学習用データを充実させることで、まだまだ改善する余地はあると思いますので、引き続き、研究していきたいと思います。

Discussion