軽量・高速・高性能と三拍子揃った日本語対応のAI(Orion-14B)で指示データセットを自動生成するメモ

はじめに

最近は大規模言語モデルの日本語データセットの生成にハマっております。
ネット上の雑多な文章よりも、AIが作った文章の方がマシではないか、という気がしてきたので、自動生成も試しています。

GPT3.5/4はかなり良い品質のデータを作ってくれますが、出力をAI学習に使う上で諸々の制約があります。

以下の記事ではいい感じにローカルLLMからデータ生成できており、いたく感銘を受けました。これを真似しながら、プラスαをやっていきます。

使うモデル

Orion-14Bを使います。

モデルの商用利用も可能なようですが、申請が必要とのことです。
出力データも似たような制約を受ける可能性がありますが、まあ頑張れば、商用でも使えると思います。

Wikipediaからの質疑生成

コード

まずは以下の記事のトレースを行います。

基本的にはコードをそのまま流用しますが、以下の通り、少し修正しました。

  • JSONのエラーを修正

    • モデルの出力が完璧ではないので、全体の半分以上の出力でjsonにエラーが出ました

      • jsonを自動修正する関数をかませることにしました

      • これで成功率が90%くらいまで上がったと思います

  • 元の文章の読み取り箇所をランダム化

    • wikipediaの本文の参照箇所をややランダム化することで、問題文にバリエーションを加える目論見です

# %%
from transformers import pipeline
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
from transformers.generation.utils import GenerationConfig
import json
import random
import string
import sys
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import time
import datasets
import re
from tqdm import tqdm

model_name = "OrionStarAI/Orion-14B-Chat"
# model_name="OrionStarAI/Orion-14B-Chat-Int4"

model = AutoModelForCausalLM.from_pretrained(model_name,
                                             trust_remote_code=True,
                                             torch_dtype=torch.bfloat16,
                                             device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
model.generation_config = GenerationConfig.from_pretrained(
    model_name,
)
model.generation_config.max_new_tokens = 5000


def gpt(utterance, theme):
    messages = [{"role": "system",
                 "content": "あなたは役に立つAIです。ユーザの質問、依頼を正確に便利に答えてください。正解がわからない場合に「正解がわかりません」と答えてください。全てJSON形式で返します。"}]
    messages.append({"role": "user", "content": utterance})
    res = model.chat(tokenizer, messages, streaming=False)

    return res


def generate_random_string(length):
    letters = string.ascii_letters
    result_str = ''.join(random.choice(letters) for i in range(length))
    return result_str

# data=datasets.load_dataset("izumi-lab/wikipedia-ja-20230720",split="train").shuffle()


# %%
dataset = datasets.load_dataset(
    "hpprc/wikipedia-20240101", split="train").shuffle()


# %%


def repair_json_string(s):
    s = re.sub(r':', ':', s)
    s = re.sub(r',', ',', s)

    # 不正なクォートを修正(必要に応じて)
    s = s.replace('”', '"').replace(
        '“', '"').replace('‘', "'").replace('’', "'")

    # 不正なクォートとコロンのパターンを修正
    s = re.sub(r'([{},])(\s*)"([^"]+)"(\s*):', r'\1 "\3":', s)  # 正しいクォートとコロン
    s = re.sub(r'"([^"]+)"(\s*):(\s*)"([^"]+)"', r'"\1": "\4"', s)  # キーと値のクォート

    s = s.replace('”', '"').replace('“', '"').replace(
        '‘', "'").replace('’', "'")  # 不正なクォートを修正
    return s


# %%
cnt = 0
ct = int(time.time())
for i in tqdm(range(10**6)):

    len_data = len(dataset)
    target_article = random.randint(0, len_data)
    row = dataset[target_article]
    if row["title"] == row["text"]:
        continue
    if len(row["text"]) < 500:
        continue

    wiki_text = row['text']
    n = 500

    if len(wiki_text) > n:
        start_index = random.randint(0, len(wiki_text) - n)
        extracted_text = wiki_text[start_index:start_index + n]
    else:
        extracted_text = wiki_text
    try:
        # res=gpt(f"{row['title']}について書かれた以下の文章を読んで先生と生徒で会話する会話文を作りなさい。\n\n▪️{row['title']}\n\n"
        #        f"{row['text'][:4096]}\n\n"
        #        '上記の文章について日本語での質問文と返答文のセットを作り、```"conversations":[{"生徒":"<質問1>","先生":"<回答1>"},{"生徒":"<質問2>",'
        #        '"先生":"<回答2>"},{"生徒":"<質問3>","先生":"<回答3>"},{"生徒":"<質問4>","先生":"<回答4>"}]```のように4つ以上の質問と答えを考え、'
        #        "それをJSON形式で返しなさい。ダブルクォーテーションは適切にエスケープしなさい\n",row['title'])
        res = gpt(f"{row['title']}内の記事で書かれた以下の文章を読んで先生と生徒で会話する会話文を作りなさい。\n\n▪️{row['title']}\n\n"
                  f"{extracted_text[:4096]}\n\n"
                  '上記の文章について日本語で質問文と返答文のセットを作り、```"conversations":[{"生徒":"<質問1>","先生":"<回答1>"},{"生徒":"<質問2>",'
                  '"先生":"<回答2>"},{"生徒":"<質問3>","先生":"<回答3>"},{"生徒":"<質問4>","先生":"<回答4>"}]```のように4つ以上の質問と答えを考え、'
                  "それをJSON形式で返しなさい。ダブルクォーテーションは適切にエスケープしなさい\n", row['title'])

        if '{"生徒": "<質問' in res or "<回答" in res:
            continue
        # print(res)
        res = repair_json_string(res)
        data = json.loads(res)

        # print(data, file=sys.stderr)
        if len(data['conversations']) < 1:
            continue
        data['title'] = row['title']
        #data['body'] = row['text']
        data['body'] = "" #jsonが重たくなるので、元のtextは消しておく
        # print(json.dumps(data, ensure_ascii=False))
        with open(f"gen_data/orin14b{ct}.txt", "a") as f:
            f.write(json.dumps(data, ensure_ascii=False)+"\n")

    except Exception as e:
        print('### エラーが発生しました。%s' % res, file=sys.stderr)
        print(e, file=sys.stderr)
        pass

    cnt += 1
    # if cnt>100000:
    #       break
    # break

# %%
wiki_text = row['text']
n = 500

# テキストがn文字以上であることを確認
if len(wiki_text) > n:
    start_index = random.randint(0, len(wiki_text) - n)
    extracted_text = wiki_text[start_index:start_index + n]

# %%
extracted_text

# %%
row["title"]

# %%

結果

A100を使うと、5-10秒に一回、データを出力できました。
4並列で10日回せば、50万件(500k)です。侮れないデータ量です。

出力例

{"conversations": [{"生徒": "西田幾多郎記念哲学館はいつ指定されたのですか?", "先生": "1969年(昭和44年)4月1日に指定されました。"}, {"生徒": "西田幾多郎記念哲学館にはどのような物が指定されていますか?", "先生": "西田幾多郎原稿、西田幾多郎書簡、西田得登書屏風六面が指定されています。"}, {"生徒": "西田幾多郎記念哲学館の周辺施設にはどのようなものがありますか?", "先生": "かほく市うみっこらんど七塚 - 海と渚の博物館、イオンモールかほく、かほく市消防本部、かほく市役所があります。"}, {"生徒": "西田幾多郎記念哲学館への交通アクセスはどうすればよいですか?", "先生": "JR七尾線宇野気駅から徒歩20分または、のと里山海道白尾インターチェンジから車3分です。"}],

蛇足: 4bit推論について

ちなみに、同モデルを4bitで読み込むと、推論時間が2倍程度に増えてしまいました。また、JSONの生成精度がかなり落ちるようで、失敗確率が50%以上に増えたた感じです。

また、公式の圧縮モデルOrion-14B-Chat-Int4も、なぜか推論に1min以上は要しました。ハードウェアの相性でしょうか。

なので結局、16bitを使っています。
(CTranslate2, ggufなどで高速化するのは有りだと思います)


Dolly datasetの自然な日本語化

パブリックな指示データセットとして有名なのは、dolly-15k-jaなどですが、これらは基本的に自動翻訳なので、品質にやや難があります。

例えば、以下の質問。

Q:車がなくても移動できる良い方法とは?
A:都市部にお住まいの方は、バスや電車などの公共交通機関を利用することができます。また、徒歩や自転車、電動スクーターなどを使って移動することもできます。

文意はわかりますが、日本語の質問・会話としては、やや唐突というか、かなり不自然です。

これを以下のプロンプトで修正してみます。


res=gpt(f""
        f"{txt[:4096]}\n\n" 
        '上記の文章の文意を忠実に守りつつも、日本語として不自然な表現を修正して質問文と返答文のセットを作り、```"conversations":[{"Q":"<質問文>","A":"<回答文>"}]```のように生成し'
        "それをJSON形式で返しなさい。ダブルクォーテーションは適切にエスケープしなさい\n")
 

出力

'\'{"conversations":[{"Q":"車がなくても移動する良い方法は?","A":"都市部にお住まいの方であれば、バスや電車などの公共交通機関を利用することができます。また、徒歩や自転車、電動スクーターなどを使って移動することもできます。"}]}\''

完璧ではありませんが、「都市部にお住まいの方であれば、」のような補足がついたり、ちょっと丁寧な感じになりました。

次の問題(オリジナル)

Q:凧って何?
A:凧は、お店や専門店で購入できる玩具です。 凧は空気より軽いもので、糸で結んで風の流れに乗って飛ばすことができます。 凧には翼があり、それが空気と反応して揚力を生み出し、飛ぶことができます。 あなたは公園やオープンエリアで凧を揚げることができ、あなたは凧を揚げるときに安全であることが近くに送電線がないことを確認したいと思うでしょう。
"

修正後
'"conversations":[{"Q":"凧って何?","A":"凧は、糸で結んで風の流れに乗せて飛ばすことができる、空気より軽い玩具です。凧には翼があり、それが空気と反応して揚力を生み出し、飛ぶことができます。"}]'

→ 勝手に文章を略したり、「凧は空気より軽い」という間違いを修正できてなかったので、やや期待外れでした。

Yahoo!知恵袋からのデータ抽出

Yahoo!知恵袋の質疑からデータを作ってみます。

こちらの質問から適当にコピペした情報を入力とします。

txt="""

1051014685さん

2024/2/3 6:53

2回答

ディズニーシーは、混雑予想に反して2024年2月1日と2日はかなり混雑したようなのですが、なぜでしょうか?

また、8日にシーに行く予定なのですが、同じようになるのでしょうか?

テーマパーク・2,884閲覧
ディズニーシー 混雑に関するQ&A

テーマパーク
至急 混雑月のディズニーシーについて 後輩と3月後半に泊まりでディズニーに行こうと話をしてるの...

ベストアンサー:混んでることを覚悟の上で行けば普通に楽しめると思います。 ディズニーが特別好きだというわけでもなく人混みが嫌い、並ぶことに抵抗があるような人だと他の所へ遊びに行った方
3
2/15 14:57
匿名投稿

テーマパーク
混雑日のディズニーシーのショーレストランについて 10:30 14:05 15:50 だとどの

ベストアンサー:シーのどのアトラクションに乗りたいかにもよりますが、効率の良い回り方は午前中の段取りが肝心なので、ショーレストランは出来るだけ遅い時間がおすすめです。 質問にある...
2
1/6 1:08
xmlns="http://www.w3.org/2000/svg">
500

テーマパーク
ディズニーシーの混雑 10月の月曜日と11月の土曜日だと、どちらの方が混雑しているでしょうか?
2
2023/9/14 7:30
xmlns="http://www.w3.org/2000/svg">
25
Q&Aをもっと見る(21,172件)

Yahoo!検索で調べてみよう

        ディズニーシー 混雑ディズニーシー

回答(2件)

ID非公開さん

2024/2/4 9:58
首都圏の私立中学・高校の入試が2月の1〜3日に集中しており、それらの学校に通う生徒が一斉に行くからではないでしょうか。
と思っていたのですが、2月2日にディズニーランドに行ったところ中高生より大人がずっと多かったですね。待ち時間も3大マウンテンは80分なんてザラでした。
チケット代が前後の日より安かったことと、2月2日が金曜日で休みをとって3連休にする人が多かったのかな…?と考えています。

NEW! この回答はいかがでしたか? リアクションしてみよう

1051014685さん

質問者2024/2/4 14:01
ありがとうございます。

そうなんですね!予想以上の混雑でびっくりしました。ご回答ありがとうございました。

Yahoo!知恵袋 公式回答AIさん

Yahoo!知恵袋 公式AI

2024/2/3 6:53
2024年2月1日と2日のディズニーシーの混雑原因は特定できませんが、週末やイベント、季節の影響などが考えられます。また、天候や新アトラクションの開業なども影響します。

8日の混雑予想については、具体的な情報がないと正確な予想は難しいです。ただし、一般的に平日は週末よりも混雑が少ない傾向にあります。混雑予想サイトをチェックすることをおすすめします。

※この回答はOpenAIのGPT-4で作成されており、最新の情報や完全な正確性等を保証するものではありません。

新機能

AI回答機能(GPT-4版)テスト実施中

詳しくはこちらから 
"""

から抜粋してみます。

res=gpt(f""
        f"{txt[:4096]}\n\n" 
        'カジュアルな表現、公序良俗に反する表現を修正しながら、質問文と返答文のセットを作り、```"conversations":[{"Q":"<質問文>","A":"<回答文>"}]```のように生成し'
        "それをJSON形式で返しなさい。ダブルクォーテーションは適切にエスケープしなさい\n")
res 

回答は、、、

'{"conversations":[{"Q":"ディズニーシーは、混雑予想に反して2024年2月1日と2日はかなり混雑したようなのですが、なぜでしょうか?","A":"首都圏の私立中学・高校の入試が2月の1〜3日に集中しており、それらの学校に通う生徒が一斉に行くからではないでしょうか。また、2月2日が金曜日で休みをとって3連休にする人が多かったのかもしれません。"},{"Q":"8日にシーに行く予定なのですが、同じようになるのでしょうか?","A":"具体的な情報がないと正確な予想は難しいですが、一般的に平日は週末よりも混雑が少ない傾向にあります。混雑予想サイトをチェックすることをおすすめします。"}]}'

と、わりといい感じでした。
この流れで、それなりの品質のデータセットを作れそうです。
(ただしデータセット公開にあたっては、著作権の問題が無視出来ないので注意)

まとめ

ローカルな大規模言語モデルでも、それなりに高品質なデータ合成ができる時代がやってきたようです。今後はいい感じに(公開)データセットを作っていきたいと思います。
(計算資源の重要性がどんどん増しますね、、)

この記事が気に入ったらサポートをしてみませんか?