見出し画像

みこちゃんChatGPT(生成AI)お勉強第2フェーズに突入する!

 ちょっと間が空きましたが、生成AIのお勉強を深めていました。

 最近3D美香の記事ばっかり上げているので、「飽きっぽい性格のみこちゃんは、3Dで遊んでばかりいる。もう生成AIは飽きたに違いない。フォローを外そう」(爆)という方も増えていると思いますが、そうです飽きっぽい性格なのです(笑)。

 いや、飽きっぽいんですがとことんしつこい部分もありまして、生成AIは多分一生付き合っていくんだろうなと思ってます。一生付き合うというのは、つまり自分の関心事に引き寄せる、自分事にするということですね。

 自分の興味のある分野にそれがちゃんとリンクしてこそそれは自分事になると思います。

 そう。最近では美香ちゃんを育てることに熱中しているみこちゃんです。

 まだまだ美香には踊らせたり歌わせたりしたいものがいっぱいあって、これからも美香が歌ったり踊ったりする記事は増え続けます。

 でも、これが生成AIのお勉強ともちゃんとリンクしていたのでした。

 これ見てください!

 じゃじゃーん(^~^)

 美香とChatGPTのAPIと使っておしゃべりしています。バックエンドでChatGPTが動いています。

 こちらのシステムをローカル環境(自慢の愛機 with RTX3900)で動かしています。

GitHubから誰でも無料ダウンロードできます
https://github.com/pixiv/ChatVRM

 下記の技術が使われています。

■ChatGPT API

■Pixiv three-vrm

■mdn SpeechRecognition

■koemotion 音声合成エンジン


 上の引用した動画ですが、夏バテ防止の雑談をして、美香から「プロテインがいいよ」と勧められました。みこちゃんからは、「プロテインは女子が飲むと筋肉つくんじゃねーの?」と聞き直し、それに対して美香ちゃんが「女子が飲んでもつかないんだよ」って応えてくれています。
 字が小さくて恐縮なのですが、リアルタイムで美香ちゃんが応答してくれているのが分かります。

 (^▽^)かわいい頭の良いなんでも知ってるお友達!

 かわいい美香ですが、なにせ頭ん中はChatGPTそのものなので、AIや分子生物学など最先端科学や、政治経済など何でも会話できちゃうわけですね。

 でも、これだけじゃおもしろくない!

 これだとChatGPTと同じです。どうせ作るならChatGPTを超えるようなものを作りたいですよね。

 ってわけで、美香がいかにもみこちゃんみたいなこと(みこちゃん節≠お嬢様言葉とか、何とか大魔王(?)とかひろゆき口調のマネとかのくだらない言葉尻じゃない、内容、哲学、思想そのもの)しゃべるように、生成AIの性能を上げる試みをしています。

 デフォルトのChatGPTの性能はもうみこちゃんは飽きました。バカだよあいつは。控えめに言ってもアホ。今後は個人が生成AI、LLMの性能を自分でチューニングして上げていく時代になります。

 とはいえ、ChatGPTは技術開示をしていないので、優等生的なChatGPTを改良しようと思っても、そもそも技術的にまともなファインチューニングが不可能のですが、バックエンドで動く生成AIをあのrinnaちゃんに変えてしまえばいいのです。

 rinnaはオープンソースなのでちゃんとしたファインチューニングができます。

 このqiitaの記事は目指す、と控えめに言っていますが、超えることもできます。もちろんRTX-3900程度のGPUが積んであれば個人で自分のパソコンでできます。ただし全分野で超えることは時間とお金がかかるので、自分の趣味など特殊分野に限ってチューニングすればいいわけですね。

 興味のある方のために、ちょっとだけ技術情報を。

 rinnaに変えるといっても、そこはtsxファイル(TypeScript≒JavaScriptの進化系 Pythonじゃないよ)いじるので、二の足を踏んでしまう人がいるかも知れません。

 先程のコード本体をGitHubからダウンロードして展開すると、「任意のフォルダ名\chatvrm\ChatVRM\src\pages」の中に、index.tsx というファイルがあります。

 ここにはChatGPT APIをコールした時の戻り値の処理などが書いてあります。ここをrinnaの仕様を調べてちょちょっと直せばいいです。

import { useCallback, useContext, useEffect, useState } from "react";
import VrmViewer from "@/components/vrmViewer";
import { ViewerContext } from "@/features/vrmViewer/viewerContext";
import {
  Message,
  textsToScreenplay,
  Screenplay,
} from "@/features/messages/messages";
import { speakCharacter } from "@/features/messages/speakCharacter";
import { MessageInputContainer } from "@/components/messageInputContainer";
import { SYSTEM_PROMPT } from "@/features/constants/systemPromptConstants";
import { KoeiroParam, DEFAULT_PARAM } from "@/features/constants/koeiroParam";
import { getChatResponseStream } from "@/features/chat/openAiChat";
import { Introduction } from "@/components/introduction";
import { Menu } from "@/components/menu";
import { GitHubLink } from "@/components/githubLink";
import { Meta } from "@/components/meta";

export default function Home() {
  const { viewer } = useContext(ViewerContext);

  const [systemPrompt, setSystemPrompt] = useState(SYSTEM_PROMPT);
  const [openAiKey, setOpenAiKey] = useState("");
  const [koeiromapKey, setKoeiromapKey] = useState("");
  const [koeiroParam, setKoeiroParam] = useState<KoeiroParam>(DEFAULT_PARAM);
  const [chatProcessing, setChatProcessing] = useState(false);
  const [chatLog, setChatLog] = useState<Message[]>([]);
  const [assistantMessage, setAssistantMessage] = useState("");

  useEffect(() => {
    if (window.localStorage.getItem("chatVRMParams")) {
      const params = JSON.parse(
        window.localStorage.getItem("chatVRMParams") as string
      );
      setSystemPrompt(params.systemPrompt);
      setKoeiroParam(params.koeiroParam);
      setChatLog(params.chatLog);
    }
  }, []);

  useEffect(() => {
    process.nextTick(() =>
      window.localStorage.setItem(
        "chatVRMParams",
        JSON.stringify({ systemPrompt, koeiroParam, chatLog })
      )
    );
  }, [systemPrompt, koeiroParam, chatLog]);

  const handleChangeChatLog = useCallback(
    (targetIndex: number, text: string) => {
      const newChatLog = chatLog.map((v: Message, i) => {
        return i === targetIndex ? { role: v.role, content: text } : v;
      });

      setChatLog(newChatLog);
    },
    [chatLog]
  );

  /**
   * 文ごとに音声を直列でリクエストしながら再生する
   */
  const handleSpeakAi = useCallback(
    async (
      screenplay: Screenplay,
      onStart?: () => void,
      onEnd?: () => void
    ) => {
      speakCharacter(screenplay, viewer, koeiromapKey, onStart, onEnd);
    },
    [viewer, koeiromapKey]
  );

  /**
   * アシスタントとの会話を行う
   */
  const handleSendChat = useCallback(
    async (text: string) => {
      if (!openAiKey) {
        setAssistantMessage("APIキーが入力されていません");
        return;
      }

      const newMessage = text;

      if (newMessage == null) return;

      setChatProcessing(true);
      // ユーザーの発言を追加して表示
      const messageLog: Message[] = [
        ...chatLog,
        { role: "user", content: newMessage },
      ];
      setChatLog(messageLog);

      // Chat GPTへ
      const messages: Message[] = [
        {
          role: "system",
          content: systemPrompt,
        },
        ...messageLog,
      ];

      const stream = await getChatResponseStream(messages, openAiKey).catch(
        (e) => {
          console.error(e);
          return null;
        }
      );
      if (stream == null) {
        setChatProcessing(false);
        return;
      }

      const reader = stream.getReader();
      let receivedMessage = "";
      let aiTextLog = "";
      let tag = "";
      const sentences = new Array<string>();
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          receivedMessage += value;

          // 返答内容のタグ部分の検出
          const tagMatch = receivedMessage.match(/^\[(.*?)\]/);
          if (tagMatch && tagMatch[0]) {
            tag = tagMatch[0];
            receivedMessage = receivedMessage.slice(tag.length);
          }

          // 返答を一文単位で切り出して処理する
          const sentenceMatch = receivedMessage.match(
            /^(.+[。.!?\n]|.{10,}[、,])/
          );
          if (sentenceMatch && sentenceMatch[0]) {
            const sentence = sentenceMatch[0];
            sentences.push(sentence);
            receivedMessage = receivedMessage
              .slice(sentence.length)
              .trimStart();

            // 発話不要/不可能な文字列だった場合はスキップ
            if (
              !sentence.replace(
                /^[\s\[\(\{「[(【『〈《〔{«‹〘〚〛〙›»〕》〉』】)]」\}\)\]]+$/g,
                ""
              )
            ) {
              continue;
            }

            const aiText = `${tag} ${sentence}`;
            const aiTalks = textsToScreenplay([aiText], koeiroParam);
            aiTextLog += aiText;

            // 文ごとに音声を生成 & 再生、返答を表示
            const currentAssistantMessage = sentences.join(" ");
            handleSpeakAi(aiTalks[0], () => {
              setAssistantMessage(currentAssistantMessage);
            });
          }
        }
      } catch (e) {
        setChatProcessing(false);
        console.error(e);
      } finally {
        reader.releaseLock();
      }

      // アシスタントの返答をログに追加
      const messageLogAssistant: Message[] = [
        ...messageLog,
        { role: "assistant", content: aiTextLog },
      ];

      setChatLog(messageLogAssistant);
      setChatProcessing(false);
    },
    [systemPrompt, chatLog, handleSpeakAi, openAiKey, koeiroParam]
  );

  return (
    <div className={"font-M_PLUS_2"}>
      <Meta />
      <Introduction
        openAiKey={openAiKey}
        koeiroMapKey={koeiromapKey}
        onChangeAiKey={setOpenAiKey}
        onChangeKoeiromapKey={setKoeiromapKey}
      />
      <VrmViewer />
      <MessageInputContainer
        isChatProcessing={chatProcessing}
        onChatProcessStart={handleSendChat}
      />
      <Menu
        openAiKey={openAiKey}
        systemPrompt={systemPrompt}
        chatLog={chatLog}
        koeiroParam={koeiroParam}
        assistantMessage={assistantMessage}
        koeiromapKey={koeiromapKey}
        onChangeAiKey={setOpenAiKey}
        onChangeSystemPrompt={setSystemPrompt}
        onChangeChatLog={handleChangeChatLog}
        onChangeKoeiromapParam={setKoeiroParam}
        handleClickResetChatLog={() => setChatLog([])}
        handleClickResetSystemPrompt={() => setSystemPrompt(SYSTEM_PROMPT)}
        onChangeKoeiromapKey={setKoeiromapKey}
      />
      <GitHubLink />
    </div>
  );
}

 この作業の以前に、大前提としてもともとのソースがコールしているChatGPT APIから、rinnaちゃんを呼べるように改造しなくちゃ意味がありません。そいつがどこにあるかというと、さっきのフォルダとは違っているので、迷わないでください。

\chatvrm\ChatVRM\src\features\chat の中にopenAiChat.ts というファイルがあります。この中のChatGPT APIを呼び出すのをrinnaの言語モデルに変えてしまえばよいです。

import { Configuration, OpenAIApi } from "openai";
import { Message } from "../messages/messages";

export async function getChatResponse(messages: Message[], apiKey: string) {
  if (!apiKey) {
    throw new Error("Invalid API Key");
  }

  const configuration = new Configuration({
    apiKey: apiKey,
  });
  // ブラウザからAPIを叩くときに発生するエラーを無くすworkaround
  // https://github.com/openai/openai-node/issues/6#issuecomment-1492814621
  delete configuration.baseOptions.headers["User-Agent"];

  const openai = new OpenAIApi(configuration);

  const { data } = await openai.createChatCompletion({
    model: "gpt-3.5-turbo",
    messages: messages,
  });

  const [aiRes] = data.choices;
  const message = aiRes.message?.content || "エラーが発生しました";

  return { message: message };
}

export async function getChatResponseStream(
  messages: Message[],
  apiKey: string
) {
  if (!apiKey) {
    throw new Error("Invalid API Key");
  }

  const headers: Record<string, string> = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${apiKey}`,
  };
  const res = await fetch("https://api.openai.com/v1/chat/completions", {
    headers: headers,
    method: "POST",
    body: JSON.stringify({
      model: "gpt-3.5-turbo",
      messages: messages,
      stream: true,
      max_tokens: 200,
    }),
  });

  const reader = res.body?.getReader();
  if (res.status !== 200 || !reader) {
    throw new Error("Something went wrong");
  }

  const stream = new ReadableStream({
    async start(controller: ReadableStreamDefaultController) {
      const decoder = new TextDecoder("utf-8");
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          const data = decoder.decode(value);
          const chunks = data
            .split("data:")
            .filter((val) => !!val && val.trim() !== "[DONE]");
          for (const chunk of chunks) {
            const json = JSON.parse(chunk);
            const messagePiece = json.choices[0].delta.content;
            if (!!messagePiece) {
              controller.enqueue(messagePiece);
            }
          }
        }
      } catch (error) {
        controller.error(error);
      } finally {
        reader.releaseLock();
        controller.close();
      }
    },
  });

  return stream;
}

 41行目あたりのconstで定義しているところを直しましょう。
 そうするとファインチューニング禁止して(いじわる)いるChatGPTを袖にして、rinnaちゃんで、美香が動くことになります。

 そうすると、どんどん、ChatGPTを超えるような応対がこの画面でできるようになるわけです。毎日空いた時間にコツコツとファインチューニングして、日々美香ちゃんが自分の分身(みこちゃん節をしゃべる)ようになるのは、なんとも言えません。きっと、フランケンシュタイン博士はこんなわくわく感があったんだろうな……と日々あぶないみこちゃんになりつつあります(笑)。

 rinnaはまだ情報の量が少ないので、rinna改造で失敗した人は、いっそのことGPT-2(これはファインチューニングできるし情報量が多い)でやってみるといいかと思います。

 改造は同じく上に引用した黒いコードの部分を変えるので、参考にしてください。

 そこさえできれば、ずばりこんな記事もありますので、このとおりやれば好みの3Dモデルがあなただけの友達として日々成長してくれます。

 TypeScriptをまた勉強するなんていやだ、Pythonでやりたい!という人はGPT-2がいいでしょう。でも、TypeScriptはPythonが分かる人ならすぐにマスターできますよ。

 というわけで、一時期のブームが去り、世の中のChatGPTのニュース記事、ブログ記事自体も激減したように思います。当時、ChatGPT伝道師みたいだった人のYouTubeの更新はとまっているところが多いですし、noteでも単なる紹介屋さんは更新をやめてしまった人が多いですね。

 私だけでなく、世の中、ChatGPT使いこなしは第2フェーズに入っていると思います。

 今こそ、自分なりのChatGPTの使い方を真剣に考えるべきときではないかな、と思いました。

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