【人工知能】深層学習で「記事タイトルを自動生成」する


# 作ったもの

深層学習を用いて、Qiita記事のタイトルを自動生成してくれるAIを試作してみました。
学習済みモデルと実行するためのソースコードも公開しましたので、**どなたでもQiita記事のタイトル自動生成をお試しいただけます**。

もちろん、本記事のタイトルもそのAIさんに生成してもらったものです。

このAI（深層学習モデル）は、下図のように、Qiitaの記事本文（Markdown形式）を与えると適したタイトルを何個も考えてくれるというものです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26062/d96c74c5-a313-3c90-9a1b-70aa48f5b94c.png" alt="Qiitaタイトル自動生成の概要図" width="1000">
なお、学習データには、[Qiitaの殿堂](http://youwht.ml)に掲載されている評価の高い記事（LGTMが50個以上）を[Qiita API](https://qiita.com/api/v2/docs)を用いてスクレイピングしたものを使いました。
つまり、**ヒットした記事のタイトルの付け方を学んだAIである**といえます。

実験的に、過去記事の本文をいくつか与えてみたところ、下記のようなタイトルが生成されました。
generatedが生成されたもの、actualが人が付けたタイトルです。

```text
generated: python pandasデータ処理メモ
actual:    [python]pandasの使い方まとめ

generated: 良いプルリクエストを書くには
actual:    github「完璧なプルリクの書き方を教えるぜ」

generated: phpのinterfaceの命名規約を調べた
actual:    人生いろいろ、インターフェイス命名規則いろいろ

generated: vue.js+typescriptでのベストプラクティスについて考えてみた
actual:    vuexによる状態管理を含む最高に快適なvue.js+typescriptの開発環境を目指す話

generated: openai gym使い方メモ
actual:    openai gym入門

generated: mysqlの文字コードとデータベース/カラムの文字コード
actual:    mysqlで文字コードをutf8にセットする

generated: ai academyのpython文法速習編とpythonプログラミング入門編
actual:    【初心者向け】無料でpythonの基本文法を5時間で学ぼう!

generated: reactのエコシステムとその概要
actual:    2018年reactとreduxのエコシステム総まとめ

generated: createjsをはじめる人向け
actual:    createjsをはじめる人が知っておくとラクなこと

generated: android studio編
actual:    iosとの比較つき!androidでこんなアプリ,こんな機能を作りたかったらこれを見ろ!作りたいアプリに対応するクラス、ライブラリのまとめ!

generated: phpで一時ファイルを作る
actual:    phpで一時的なファイルポインタを扱う方法

generated: ossを毎日のように読まないでいる自分を救う方法
actual:    「ソースコード全部読まなきゃ病」と闘う方法

generated: dfs(深さ優先探索)超入門!〜グラフ理論の世界への入口〜【前編】
actual:    bfs(幅優先探索)超入門!〜キューを鮮やかに使いこなす〜

generated: ノンプログラマーが3ヶ月でwebサービスをoss化した話
actual:    はじめてのossリリース記〜なぜ無料でソースコードを公開するのか?

generated: 最短コースで日本語word2vecを使う
actual:    15分でできる日本語word2vec

generated: redis-stackでredis-stack overflowする方法
actual:    redisのパターンに一致するキーを削除

generated: 50ページ以上のvue.jsアプリケーションを爆速で開発するためのディレクトリ構成
actual:    大規模vue.jsアプリを開発するときのディレクトリ構成考えた

generated: angularjs用のyeomanをインストールする
actual:    yeomanを使ったangularjsアプリをチームで共同開発して公開するまで

generated: 円グラフを描画するライブラリをまとめてみた
actual:    無料でここまでできる!iosでグラフ/チャートを描くためのライブラリ

generated: はじめてのsourcetree(gitとgithubの違い)
actual:    sourcetreeとgithubでgitの練習環境をつくる

generated: エンジニアなら知っておきたい、見た目がイケてるターミナルにしよう
actual:    お前らのターミナルはダサい
```

結構ちゃんとしていますね。何となく、生成されたタイトルの方がやや端的で控えめですかね？

それでは、このQiita記事のタイトル自動生成の実行方法の詳細と、学習方法の概略を説明していきます。


# Qiita記事のタイトル自動生成

タイトルの自動生成の実行はとても簡単です。
一言で言えば、次のNotebookをGoogle Colaboratoryで開き、全セルを実行するだけです。

- タイトル自動生成のNotebook: https://github.com/sonoisa/qiita-title-generation/blob/main/T5_ja_qiita_title_generation.ipynb
    - Notebookを直接Colaboratoryで開くリンク: https://colab.research.google.com/github/sonoisa/qiita-title-generation/blob/main/T5_ja_qiita_title_generation.ipynb

以下、コードの意味を順番に説明していきます。


## 前提知識

- Python/PyTorchの基礎知識
- [Google Colaboratory](https://colab.research.google.com/?hl=ja)を使い方


## 準備

まず、依存ライブラリをインストールします。
[transformers](https://huggingface.co/transformers/)は各種[Transformer](https://qiita.com/omiita/items/07e69aef6c156d23c538)系モデルが実装されたライブラリです。
[sentencepiece](https://github.com/google/sentencepiece)はトークナイザー[SentencePiece](https://qiita.com/taku910/items/7e52f1e58d0ea6e7859c)のライブラリです。

```bash:In[1]
!pip install -qU torch==1.7.1 torchtext==0.8.0 torchvision==0.8.2
!pip install -q transformers==4.2.2 sentencepiece
```

## 学習済みモデルの読み込み

次のコードを実行し、学習済みモデルを読み込みます。
学習済みモデルは https://huggingface.co/sonoisa/t5-qiita-title-generation から自動的にダウンロードされます。

```python:In[2]
import torch
from torch.utils.data import Dataset, DataLoader
from transformers import T5ForConditionalGeneration, T5Tokenizer
from transformers import AdamW,get_linear_schedule_with_warmup

# 学習済みモデルをHugging Face model hubからダウンロードする
model_dir_name = "sonoisa/t5-qiita-title-generation"

# トークナイザー（SentencePiece）
tokenizer = T5Tokenizer.from_pretrained(model_dir_name, is_fast=True)

# 学習済みモデル
trained_model = T5ForConditionalGeneration.from_pretrained(model_dir_name)

# GPUの利用有無
USE_GPU = torch.cuda.is_available()
if USE_GPU:
  trained_model.cuda()
```

この深層学習モデルは、日本語コーパスを用いて事前学習したT5モデルを今回のタイトル生成タスク向けに転移学習したものです。

- T5（Text-to-Text Transfer Transformer）とは: https://www.ogis-ri.co.jp/otc/hiroba/technical/similar-document-search/part7.html

今回のモデルの入出力は次の形式になっています。

- **入力**: 文字列"body: {body}"をトークナイズしたもの
- **出力**: 文字列"title: {title}"をトークナイズしたもの

ここで、{body}はMarkdown形式の記事本文を前処理した文字列、{title}は生成されたタイトルです。


## 前処理の定義

Qiitaの記事本文（Markdown形式）をモデルに入力できる形式に変換する前処理を定義します。
表記揺れの軽減と、余計な文字列の削除を行い、"body: "という接頭辞を追加します。

### 文字列の正規化

表記揺れを減らします。
今回は以下のコードのように[neologdの正規化処理](https://github.com/neologd/mecab-ipadic-neologd/wiki/Regexp.ja)を一部改変したものを利用します。
処理の詳細はリンク先を参照してください。

```python:In[3]
# https://github.com/neologd/mecab-ipadic-neologd/wiki/Regexp.ja から引用・一部改変
from __future__ import unicode_literals
import re
import unicodedata

def unicode_normalize(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(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(s):
    s = s.strip()
    s = unicode_normalize('０-９Ａ-Ｚａ-ｚ｡-ﾟ', 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)  # normalize tildes (modified by Isao Sonobe)
    s = s.translate(
        maketrans('!"#$%&\'()*+,-./:;<=>?@[¥]^_`{|}~｡､･｢｣',
              '！”＃＄％＆’（）＊＋，－．／：；＜＝＞？＠［￥］＾＿｀｛｜｝〜。、・「」'))

    s = remove_extra_spaces(s)
    s = unicode_normalize('！”＃＄％＆’（）＊＋，－．／：；＜＞？＠［￥］＾＿｀｛｜｝〜', s)  # keep ＝,・,「,」
    s = re.sub('[’]', '\'', s)
    s = re.sub('[”]', '"', s)
    return s
```


### Markdownのクリーニング

タイトルを考える上で関係のなさそうな文章を削る処理を行います。
以下のノイズとなるデータを削除し、タブや改行を空白文字にしたり、文字を小文字に揃える等の処理を行います。

- ソースコード
- URLやリンク
- 画像

現状、img以外のHTML要素を残していますが、タイトルに関係なさそうな要素を削ると精度が上がるかもしれません。

```python:In[4]
import re

CODE_PATTERN = re.compile(r"```.*?```", re.MULTILINE | re.DOTALL)
LINK_PATTERN = re.compile(r"!?\[([^\]\)]+)\]\([^\)]+\)")
IMG_PATTERN = re.compile(r"<img[^>]*>")
URL_PATTERN = re.compile(r"(http|ftp)s?://[^\s]+")
NEWLINES_PATTERN = re.compile(r"(\s*\n\s*)+")

def clean_markdown(markdown_text):
    markdown_text = CODE_PATTERN.sub(r"", markdown_text)
    markdown_text = LINK_PATTERN.sub(r"\1", markdown_text)
    markdown_text = IMG_PATTERN.sub(r"", markdown_text)
    markdown_text = URL_PATTERN.sub(r"", markdown_text)
    markdown_text = NEWLINES_PATTERN.sub(r"\n", markdown_text)
    markdown_text = markdown_text.replace("`", "")
    return markdown_text

def normalize_text(markdown_text):
    markdown_text = clean_markdown(markdown_text)
    markdown_text = markdown_text.replace("\t", " ")
    markdown_text = normalize_neologd(markdown_text).lower()
    markdown_text = markdown_text.replace("\n", " ")
    return markdown_text

def preprocess_qiita_body(markdown_text):
    return "body: " + normalize_text(markdown_text)[:4000]
```

## 後処理の定義

モデルの出力は"title: {title}"という形式ですので、後処理として余計な"title: "を削除するようにします。

```python:In[5]
def postprocess_title(title):
  return re.sub(r"^title: ", "", title)
```

これで必要な前処理、後処理を定義できました。


## タイトル生成の対象となる記事本文の定義

今回は下記のような記事に対するタイトル生成を試してみます。  
中身を色々変えて試してみてください。

```python:In[6]
qiita_body = """
AIの進歩はすごいですね。

今回は深層学習を用いて、記事（Qiita）のタイトルを自動生成してくれるAIさんを試作してみました。
この実験は自然言語処理について新人さんに教えるためのハンズオンネタを探索する一環で行ったものになります。

作ったAIは、Qiitaの記事本文（少し前処理したテキスト）を与えると、適したタイトル文字列を作文して返してくれるというものです。

なお、学習データは（2019年頃に）Qiitaの殿堂を入り口にして、評価の高い記事（いいねが50個以上）をスクレイピングしたものを使いました。
つまりヒットした記事のタイトルの付け方を学んだAIであるといえます。

* もう少し詳細:
  * 学習データの例:
    * 入力: "body: hiveqlではスピードに難を感じていたため、私もprestoを使い始めました。 mysqlやhiveで使っていたクエリ..."
    * 出力: "title: hadoop利用者ならきっと知ってる、hive/prestoクエリ関数の挙動の違い"
  * 学習方法: 独自に作った日本語T5の事前学習モデルをこの学習データを用いて転移学習

以下、結果（抜粋）です。generatedが生成されたもの、actualが人が付けたタイトルです。
"""
```

この文章に対して前処理をすると次のようになります。これをトークナイズしたものがモデルの入力になります。

```python:In[7]
preprocess_qiita_body(qiita_body)
```
```text:Out[7]
body: aiの進歩はすごいですね。 今回は深層学習を用いて、記事(qiita)のタイトルを自動生成してくれるaiさんを試作してみました。 この実験は自然言語処理について新人さんに教えるためのハンズオンネタを探索する一環で行ったものになります。 作ったaiは、qiitaの記事本文(少し前処理したテキスト)を与えると、適したタイトル文字列を作文して返してくれるというものです。 なお、学習データは(2019年頃に)qiitaの殿堂を入り口にして、評価の高い記事(いいねが50個以上)をスクレイピングしたものを使いました。 つまりヒットした記事のタイトルの付け方を学んだaiであるといえます。 *もう少し詳細: *学習データの例: *入力: "body: hiveqlではスピードに難を感じていたため、私もprestoを使い始めました。 mysqlやhiveで使っていたクエリ..." *出力: "title: hadoop利用者ならきっと知ってる、hive/prestoクエリ関数の挙動の違い" *学習方法: 独自に作った日本語t5の事前学習モデルをこの学習データを用いて転移学習 以下、結果(抜粋)です。generatedが生成されたもの、actualが人が付けたタイトルです。
```


## タイトルの自動生成を実行

これで必要な処理とデータが揃いました。
それでは記事に合うタイトルを10個、自動生成してみます。

以下のコードではタイトルの多様性を生むために色々generateメソッドのパラメータを設定しています。
望ましい結果が生成されない場合は、温度と2種類のペナルティの値を変えてみるといいかもしれません。各種パラメータの詳細は下記リンク先を参照してください。

https://huggingface.co/transformers/main_classes/model.html#transformers.generation_utils.GenerationMixin.generate

```python:In[8]
MAX_SOURCE_LENGTH = 512  # 入力される記事本文の最大トークン数
MAX_TARGET_LENGTH = 64   # 生成されるタイトルの最大トークン数

# 推論モード設定
trained_model.eval()

# 前処理とトークナイズを行う
inputs = [preprocess_qiita_body(qiita_body)]
batch = tokenizer.batch_encode_plus(
    inputs, max_length=MAX_SOURCE_LENGTH, truncation=True, 
    padding="longest", return_tensors="pt")

input_ids = batch['input_ids']
input_mask = batch['attention_mask']
if USE_GPU:
  input_ids = input_ids.cuda()
  input_mask = input_mask.cuda()

# 生成処理を行う
outputs = trained_model.generate(
    input_ids=input_ids, attention_mask=input_mask, 
    max_length=MAX_TARGET_LENGTH,
    return_dict_in_generate=True, output_scores=True,
    temperature=1.0,          # 生成にランダム性を入れる温度パラメータ
    num_beams=10,             # ビームサーチの探索幅
    diversity_penalty=1.0,    # 生成結果の多様性を生み出すためのペナルティ
    num_beam_groups=10,  　　　# ビームサーチのグループ数
    num_return_sequences=10,  # 生成する文の数
    repetition_penalty=1.5,   # 同じ文の繰り返し（モード崩壊）へのペナルティ
)

# 生成されたトークン列を文字列に変換する
generated_titles = [tokenizer.decode(ids, skip_special_tokens=True, 
                                     clean_up_tokenization_spaces=False) 
                    for ids in outputs.sequences]

# 生成されたタイトルを表示する
for i, title in enumerate(generated_titles):
  print(f"{i+1:2}. {postprocess_title(title)}")
```
```text:Out[8]
 1. 深層学習でqiitaのタイトルを自動生成してくれるaiを試作した
 2. aiの進化はすごい。記事タイトル自動生成のaiを試作した
 3. ディープラーニングで記事のタイトルを自動生成してくれるaiを試作した
 4. ディープラーニングで記事のタイトルを自動生成してくれるaiを試作した。
 5. aiがすごい勢いで記事のタイトルを自動生成するaiを試作した
 6. 【人工知能】深層学習で「記事タイトルを自動生成」する
 7. deep learningでqiitaのタイトルを自動生成するaiを作ってみた
 8. deep learningでqiitaのタイトルを自動生成するaiを作ってみた。
 9. 「記事のタイトルを自動生成してくれるai」を作ってみた。(結果)
10. 「記事のタイトルを自動生成してくれるai」を作ってみた
```

注目すべき点は、**本文では「ディープラーニング」も「人工知能」も使ってない**ことです。
ちゃんと何のトピックについて書かれているのか把握した上で柔軟に言葉を選んでタイトルを生成しているようですね。すごい・・・。



# 学習方法（概略）

このQiita記事のタイトル自動生成に用いた深層学習モデルは以下のステップで作りました。

1. 日本語コーパス（日本語Wikipedia等）を用いたSentencePieceと日本語T5モデルの事前学習を実行する（[参考](https://www.ogis-ri.co.jp/otc/hiroba/technical/similar-document-search/part7.html)）。
2. [Qiitaの殿堂](http://youwht.ga)に掲載されている（2019年頃までの）評価の高い記事（LGTMが50個以上）を[Qiita API](https://qiita.com/api/v2/docs)を用いてスクレイピングする（約8,000件）。
3. 転移学習用に2のデータを整形する（前述の前処理を参照）。タイトルと同じ文字列が本文にある記事と、タイトルが同じ記事がある記事は除外する。
4. 転移学習を実行する。学習用Colab Notebook: https://github.com/sonoisa/qiita-title-generation/blob/main/T5_ja_training_qiita_title_generator.ipynb


# 免責事項

著者は本記事を掲載するにあたって、その内容、機能等について細心の注意を払っておりますが、内容が正確であるかどうか、安全なものであるか等について保証をするものではなく、何らの責任を負うものではありません。
本記事内容のご利用により、万一、ご利用者様に何らかの不都合や損害が発生したとしても、著者や著者の所属組織（日鉄ソリューションズ株式会社（NSSOL））は何らの責任を負うものではありません。
大規模コーパスを用いて事前学習をしているため、学習データに由来する生成傾向の偏りがあったり、もしかしたら不適切な文言が生成される可能性がなくもないです。お気を付けください。


# つづく？

今回、記事本文からタイトルを自動生成してみましたが、
実は、その逆問題である、タイトルから記事本文を自動生成するモデルも作ってみています。
その話も記事にするかもしれません。