見出し画像

今日からはじめるAI文芸実践入門:AI文房具編(1)「かぶらないノート」をつくる

はじめに

こんにちは、メディア研究開発センターの浦川です。

今回「今日からはじめるAI文芸実践入門:AI文房具編(1)『 かぶらないノート』をつくる」と題して、機械学習モデルを応用したテキストエディタの実装について書いていきます。

近頃ではMidjourneyStable Diffusionなど、機械学習モデル(AI)を用いた創作行為が注目を集めています。これらの利用法は〈あらかじめ用意されたモデルに対する入力(プロンプトと呼ばれます)に工夫を加え欲しい出力を得る〉といったかたちをとることが多いです。

このアプローチはプロンプトエンジニアリングとも呼ばれ、それだけで研究対象になるような奥の深い世界ですが、AIを創作に利用する方法は他にも考えられそうです。

ということで今回、〈モデルの挙動を理解しながら、じぶんの目的にあった道具をつくる〉という方法をとりながら、機械学習モデルを応用した「もの書き」の道具=AI文房具をつくってみようとおもいます。

なお、これまでの「AI文芸実践入門」は以下からお読みいただけます。


AI文房具「かぶらないノート」

とつぜんですがみなさん、人と「かぶっちゃう」ことって、好きですか。

好き、という方がおられるかもしれません。他人と思っていたことや感じていたことが似ていて、嬉しく思ったり素直に驚いたりする瞬間というのは、日常生活においてたしかにあるような気がします(例:夕食に食べたいメニューが同じだった、テレビを見ていて笑うポイントが同じだった、書棚におかれた一冊の本へふたりが同時に手を伸ばし「あっ」ってなった)。

一方で、なにか新しいものをつくる〈創作〉の場において、他人(また過去の自分)とかぶってしまうということは、多くの場合において避けたい事態の一つといえるでしょう。

そこで今回、すでに書かれているテキストと似た内容を書くことのできない「かぶらないノート」をつくっていこうとおもいます。

つくりたいもののイメージを、以下にまとめてみます。

1. まっさらな状態では、入力内容をそのまま保存
2. 以降は新たな入力がある度に、これまでの履歴との類似度を計算
3. どの履歴とも類似度が低いときのみ、入力を新たな内容として記録する

こんなノートを、機械学習モデルの力を借りながら、つくってみましょう。

ちなみに完成した「かぶらないノート」は、こんなかんじになりました。

「かぶらないノート」

チャット形式で入力を受け付け、新しい内容には「いいじゃん」と返答し採用、「かぶり」のあるものに対しては似ているものを示しながら却下する、といったかたちでつくってみました。

つくってみましょう

今回の「ノート」では、新たに入力される文と、これまで入力した文の類似度を測る必要があります。これを、次節で説明するSentence-BERTを用いておこないます。

また実装には、gradioとよばれるライブラリを用います。これは、機械学習モデルを用いたデモを簡単につくることのできる優れものです。

文の類似度を計算するための機械学習モデル

文の類似度を得るために、文の埋め込み表現を利用するという方法があります。埋め込み表現とは、対象のもの(今回は文)を高次元ベクトル空間の一点に埋め込むことをいいます。

埋め込み表現のイメージ

この埋め込み表現の計算に機械学習モデルを利用することができ、〈似た文は似たベクトルを持つ=ベクトル空間の中で近くに位置する〉というように学習したモデルがあれば、これを用いて文の類似度を測ることができます。

 Sentence-BERTはまさにそういったモデルで、文のペアを用いる学習によってこれを作成します。

データは(A)文ペアとその類似度を実数値で表したもの(B)文ペアとその関係〈含意/矛盾/どちらでもない〉がラベル付けされたもの (C)ある文aと近い文b、近くない文cを集めたもの のいずれかを用いて、2文もしくは3文間の関係を学習してモデルがつくられます。

学習データの例。(A)はThe STS Benchmarkから引用のうえ翻訳したもの。(B)は日本語SNLI(JSNLI)データセットからの引用。(C)は論文に掲載の例を引用のうえ翻訳。

今回は、公開されている以下の日本語版Sentence-BERTを使用します。

使い方はリンク先の情報を参照いただきつつ……
たとえば、以下のようにしてテキストの類似度を計算できます。

# 入力文
input = "きのうより寒い"
# 入力との距離を測る文配列
sents = ["きのうは暑かった", "きょうはとても寒い", "つよい雨が降っている"]

# 埋め込み表現の計算
input_e = model.encode([target]).detach().numpy()
sents_e = model.encode(sents).detach().numpy()

# inputの埋め込みと各文の埋め込み表現とのcos距離の計算
dists = scipy.spatial.distance.cdist(input_e, sents_e, metric="cosine")[0]

# sentsの各文と、各文とのcos距離の出力
for i, d in enumerate(dists):
  print(sents[i], d)

# output
# きのうは暑かった 0.4983304807797453
# きょうはとても寒い 0.3229431806926678
# つよい雨が降っている 0.8246289057571479

ここでは "きのうより寒い" という文と["きのうは暑かった", "きょうはとても寒い", "つよい雨が降っている"]の3つの文との類似度をcos距離により計算しています。

今回計算されるcos距離は、類似度が高ければ高いほど0に近づく値を取ります。

結果を見ると "きょうはとても寒い"という文との値が最も0に近く、 "きのうより寒い" との類似度がいちばん高いということがわかりますね。これは直感にもあう結果のようにおもいます。

ということで、Sentence-BERTを用いて文の類似度を計算する準備が整いました。

続いて、このモデルをつかったエディタをつくっていきます。

ちなみに、ご自身でSentence-BERTをつくられる場合には、sentence-transformersと呼ばれるフレームワークが役立ちます。具体的な方法については、たとえば以下のリンクが参考になるでしょう。

gradioによるエディタの実装

gradioは、機械学習モデルを用いたデモ(webアプリケーション)をかんたんに実装できるpythonライブラリです。

アプリケーションを組み立てるための要素が揃っており、たとえば以下のように、とてもシンプルなコードで「ユーザがお絵描きしたものを推論するデモ」をつくることができます。

これは猫です

上図では推論を実行する関数の中身は省略されていますが、ここはデモを組み立てる前に「すでにできている要素」ともいえるでしょう。

さて今回は、最初にご紹介したようにチャットボットのようなUIで

  •  入力文を受け付け

    • 過去に類似する内容がひとつでもあれば却下し最も近いものを提示

    • なければ採用し記録する

といった機能をもつデモをつくります。

全体のコードは、以下のようになりました。

import gradio as gr
from transformers import BertJapaneseTokenizer, BertModel
import torch
import scipy.spatial

MODEL_NAME = "sonoisa/sentence-bert-base-ja-mean-tokens-v2"
COS_THRESH = 0.35 # cos距離のしきい値(この値より低い場合は類似として却下する)

class SentenceBertJapanese:
    # 省略 (https://huggingface.co/sonoisa/sentence-bert-base-ja-mean-tokens-v2 参照)


with gr.Blocks() as demo:    
    model = SentenceBertJapanese(MODEL_NAME) # Sentence-BERTモデル

    chat_history = gr.State([]) # チャット履歴
    exclusions = gr.State([]) # 類似度計算にて除外するワード   
    accepts = gr.State([]) # 採用された内容   

    inp = gr.Textbox(label="input") # 入力を受け付けるテキストボックス
    exc = gr.Textbox(label="excluded words") # 除外ワードを受け付けるテキストボックス
    btn = gr.Button("send") # 入力内容の送信ボタン
    chatbot = gr.Chatbot().style(color_map=("green", "pink")) # チャット形式のUI要素
    out = gr.Textbox(label="output") # 採用された内容を出力するテキストボックス

    
    def check_sim(txt, accepts):
        """
       類似する内容がないかチェックする
        """
        if len(accepts) < 1:
            return None, True

        def exclude(txt):
            for exc in exclusions.value:
                txt = txt.replace(exc, '_')
            return txt
       
        _txt = exclude(txt)
        _accepts = [exclude(o) for o in accepts]

        txt_e = model.encode([_txt]).detach().numpy()
        accepts_e = model.encode(_accepts).detach().numpy()
        dists = scipy.spatial.distance.cdist(txt_e, accepts_e, metric="cosine")[0]

        outputs = [(d, accepts[i]) for i, d in enumerate(dists)]
        outputs = sorted(outputs, key=lambda x: x[0])      
        return outputs, all([o[0] >= COS_THRESH for o in outputs])
    

    def chat(message, history, accepts): 
        """
       送信ボタンが押されたときに呼ばれるイベント関数
        """        
        v, flag = check_sim(message, accepts)  
        if flag:
            accepts.append(message)
            response = 'いいじゃん'
        else:
            response = 'これと似ちゃってる: ' + str(v[0])
        history.append((message, response))
        return history, history, '\n'.join(accepts)


    def add_exc(txt): 
        """
       除外ワードが設定されたときに呼ばれるイベント関数
        """
        exclusions.value.clear()
        for x in txt.split(','):
            exclusions.value.append(x)
        return ','.join(exclusions.value)


    # 送信ボタンが押された際のイベント関数登録
    btn.click(
        chat, # 関数名
        [inp, chat_history, accepts], # 関数の入力元
        [chat_history, chatbot, out] # 関数の出力先
        )


    # 除外ワードが設定された際のイベント関数登録
    exc.submit(
        add_exc,
        [exc],
        [exc]
    )
    

demo.launch()

実際につかう際には、たとえばあるPR記事の見出しに入れたい〈商品名〉や、またそれが連載ものであればその〈連載タイトル〉など、どうしてもかぶってしまうキーワードのようなものがあるでしょう。

そこで〈除外ワード〉を設定するテキストボックスを設け、類似度計算の際に設定した語を_(アンダースコア)で置き換えるようにしました。

このようにすることで、あらかじめ分かっている「かぶってもいい表現」の影響がなるべく小さくなることを期待します。

除外ワードの設定

つかってみましょう

では、今回つくった「かぶらないノート」を実際につかってみましょう。

見出しをつくる

まずはじめに、「見出しをつくる」ためにこのノートをつかってみます。

今回は以下の記事の見出しを、改めて考えてみましょう。新たなものが10案だせるまで、がんばってみます。

はじめに、見出しに入れたい単語「朝日新聞Playground」を除外ワードとして設定の上、現在の見出しを入力します。

最初の入力

あとはひたすら、新たな案を投げていきます。

これは似てる
「あそぶ」から「たのしむ」へ
問いかけ系へ
「技術」&「あそび」かぶり

……結果、以下の案をつくることができました。

採用された見出し案

最後の方にいくにつれ、「指先に最新技術」とか「あつまれ!」とか、ちょっと大げさ&投げやりになっているようにもみえますが、たしかにことなる方向性の見出し案を出せた気がします。

短歌をつくる

また今度は別のケースとして「短歌をつくる」ことを考えてみましょう。

今回は〈上の句まではできていて、下の句の方向性に悩んでいる〉という設定とします。

上の句を除外ワードとして設定し、下の句だけ変えたものをどんどん入力していきます。

やっていきます
このへんはすいすい
たしかに似ている
これは意外と通った

……結果、以下の歌をつくることができました。

採用された歌

見出しより自由度が高いためか、すいすいと採用されていった印象です。

一方で「塗りつぶしてく」などの表現の重複や「余白/白紙」といった類似語にかんしてはスルーされていますね。このあたりは、しきい値を変化させていくことが必要かもしれません。

ともあれ、いくつか方向性のちがう歌が残せたように感じます。

おわりに

今回は「今日からはじめるAI文芸実践入門:AI文房具編 (1) 『かぶらないノート』をつくる」と題して、機械学習モデルを応用したエディタをつくってみました。

実際につかってみると、たしかに「かぶらない」内容を書くことができたようにおもえます。また「毎回の入力になにか反応がある」というだけでもなんというか……作業における孤独感みたいなものが和らぐのか、いつもとちがった集中ができた気がします。今回は一人でつかってみましたが、複数人で書いてみるというのもよさそうです。

一方で、アイデアの「かぶり」は書かれたテキストの表層以外にも現れるような気もします。加えて今回〈除外ワード〉なるものを設けましたが、これは学習時にはない設定であり、モデルが期待する入力とは性質がことなるものとなっている可能性があります。これがほんとうに意図した挙動となっているのかについては、さらなる検証またあらたな研究の余地があるでしょう。

我々メディア研究開発センターでは、TSUNAに代表される見出し生成といったタスクのほか、今回のような言語モデルの創造的な応用に関しても日々研究を行なっております。興味を持たれた方は、ぜひこちらまでご連絡ください。

(メディア研究開発センター・浦川通)