qinfengge

qinfengge

醉后不知天在水,满船清梦压星河
github
email
telegram

春のAI (九) マルチモーダル

まず、多モーダルとは何かを説明します:人間の学習方法を想像してみてください。視覚、聴覚、触覚があります。この中で最も重要なのは視覚です。機械は見ることができるのでしょうか?もちろんです。簡単に言えば、AI に見る、聞く、触れることをさせるのが多モーダルです。

人間は知識を、複数のデータ入力モードを同時に処理します。私たちの学び方や経験はすべて多モーダルです。私たちは視覚だけ、音声だけ、テキストだけではありません。
人間は同時に複数のデータ入力モードで知識を処理します。私たちの学び方や経験はすべて多モーダルです。私たちは視覚だけでなく、音声やテキストも持っています。

これらの学習の基本原則は、近代教育の父であるジョン・アモス・コメニウスによって、1658 年の著作「Orbis Sensualium Pictus」で明言されました。
近代教育の父であるジョン・アモス・コメニウスは、1658 年の著作「Orbis Sensualium Pictus」でこれらの学習の基本原則を明らかにしました。

image

「自然に関連するすべてのものは、組み合わせて教えられるべきである」
「自然に関連するすべてのものは、組み合わせて教えられるべきです」

マルチモーダリティ API#

OpenAI を例にとると、現在多モーダルをサポートしているモデルはあまり多くなく、基本的には最新の gpt-4-visual-previewgpt-4o だけです。詳細な説明は公式文書にあります。

以下は公式の 2 つの例です:

byte[] imageData = new ClassPathResource("/multimodal.test.png").getContentAsByteArray();

var userMessage = new UserMessage("この画像に何が見えるか説明してください?",
        List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)));

ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
        OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()).build()));
var userMessage = new UserMessage("この画像に何が見えるか説明してください?",
        List.of(new Media(MimeTypeUtils.IMAGE_PNG,
                "https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png")));

ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
        OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_O.getValue()).build()));

メディア情報が UserMessage に設定されていることがわかります。これはユーザーの入力を表しています。以前はテキストの入力のみを使用していましたが、ファイルの入力もサポートされていることがわかりました。

ここには new Media() メソッドもあり、上記の 2 つの方法で異なる 2 種類のパラメータが渡されています。最初のものは byte[] 配列を渡し、2 番目のものは URL リンクを渡します。

実際、このメソッドは 3 種類のパラメータをサポートしています。

image

ただし、最初の byte[] 配列の方法はすでに非推奨とされています。

原理がわかったので、以前のストリーミング出力のコードを少し変更すればよいです:

/**
 * @author LiZhiAo
 * @date 2024/6/24 16:09
 */

@RestController
@RequestMapping("/multi")
@RequiredArgsConstructor
@CrossOrigin
public class MultiController {

    private final OpenAiConfig openAiConfig;

    private static final Integer MAX_MESSAGE = 10;

    private static Map<String, List<Message>> chatMessage = new ConcurrentHashMap<>();

    /**
     * 提示語を返す
     * @param message ユーザーが入力したメッセージ
     * @return Prompt
     */
    @SneakyThrows
    private List<Message> getMessages(String id, String message, MultipartFile file) {
        String systemPrompt = "{prompt}";
        SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemPrompt);

        Message userMessage = null;
        if (file == null){
             userMessage = new UserMessage(message);
        }else if (!file.isEmpty()){
            userMessage = new UserMessage(message, List.of(new Media(MimeType.valueOf(file.getContentType()), file.getResource())));
        }

        Message systemMessage = systemPromptTemplate.createMessage(MapUtil.of("prompt", "あなたは役立つAIアシスタントです"));

        List<Message> messages = chatMessage.get(id);

        // メッセージが取得できない場合、新しいメッセージを作成し、システムの提示とユーザーの入力メッセージをメッセージリストに追加します
        if (messages == null){
            messages = new ArrayList<>();
            messages.add(systemMessage);
            messages.add(userMessage);
        } else {
            messages.add(userMessage);
        }

        return messages;
    }

    /**
     * 接続を作成
     */
    @SneakyThrows
    @GetMapping("/init/{message}")
    public String init() {
        return String.valueOf(UUID.randomUUID());
    }

    @PostMapping("/chat/{id}/{message}")
    @SneakyThrows
    public SseEmitter chat(@PathVariable String id, @PathVariable String message,
                       HttpServletResponse response, @RequestParam(value = "file", required = false) MultipartFile file ){

        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");

        SseEmitter emitter = SseEmitterUtils.connect(id);
        List<Message> messages = getMessages(id, message, file);
        System.err.println("chatMessageのサイズ: " + messages.size());
        System.err.println("chatMessage: " + chatMessage);

        if (messages.size() > MAX_MESSAGE){
            SseEmitterUtils.sendMessage(id, "対話回数が多すぎます。後で再試行してください🤔");
        } else {
            // モデルの出力ストリームを取得
            Flux<ChatResponse> stream = openAiConfig.openAiChatClient().prompt(new Prompt(messages)).stream().chatResponse();

            // ストリーム内のメッセージをSSEで送信
            Mono<String> result = stream
                    .flatMap(it -> {
                        StringBuilder sb = new StringBuilder();
                        System.err.println(it.getResult().getOutput().getContent());
                        Optional.ofNullable(it.getResult().getOutput().getContent()).ifPresent(content -> {
                            SseEmitterUtils.sendMessage(id, content);
                            sb.append(content);
                        });

                        return Mono.just(sb.toString());
                    })
                    // メッセージを文字列に結合
                    .reduce((a, b) -> a + b)
                    .defaultIfEmpty("");

            // メッセージをchatMessageのAssistantMessageに保存
            result.subscribe(finalContent -> messages.add(new AssistantMessage(finalContent)));

            // メッセージをchatMessageに保存
            chatMessage.put(id, messages);
        }
        return emitter;
    }
}

最終結果は以下の通りです:

image

image

フロントエンドはもう変更するのが面倒なので、このままでいいです🥱

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。