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

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

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