白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

未使用のラベル(unused label)と返ってこない値

こんにちは。
最近の太巻きはだんだん豪華になって太くなっていますが、切らずに無言で食べるにはもはや苦行なんじゃないかなと思います。

さて、罠を踏んだので備忘録。
わかるひとはタイトル見たら「あー、あれね」ってわかりそう。

先に結論

おそらくこういうコードを書いていると思うので、

const func = () => { hoge: "fuga" };

こうしましょうというお話です。

const func = () => ({ hoge: "fuga" });

なんでこんなことになるのかをちょっと見ていきます。

関数の宣言の仕方あれこれ

まずJavaScriptには関数定義の仕方がいろいろあるのでそれを見ていきます。

関数宣言

一番一般的なのがこれですかね。

function sum(a, b) {
    return a + b;
}

functionキーワードに続いて関数の名前を書くタイプです。TypeScriptだと型も書けます。

function sum(a: number, b number) : number {
    return a + b;
}
関数式

関数宣言は文ですが、関数は式にもできます。関数を第一級オブジェクトとして扱える言語だとよくあるやつですね。

const func1 = function sum(a, b) {
    return a + b;
};

ちなみに名前は省略できます。名前のない関数は無名関数とか匿名関数と言います。

const func2 = function (a, b) {
    return a + b;
};

というか式としては普通は省略する気がします。名前つけるとしたら再帰呼び出しするときとかですかね。自分の名前がわからないので。

console.log(func1.name);  // sum
console.log(func2.name);  // func2

ちなみに無名関数のまま即時実行もできて、こういう時は即時実行関数とか呼ばれる気がします。

const sum = (function (a, b) {
    return a + b;
})(1, 2);
アロー関数

いわゆるラムダ式とか呼ばれるあれです。ちなみにJavaのアロー演算子は「->」でJavaScriptは「=>」。

const sum = (a, b) => {
    return a + b;
};

返す値が1つの時にはカッコが省略できて、直接戻り値が書けます。
戻り値がなくて処理だけする場合には素直にカッコを書きましょう。

const sum = (a, b) => a + b;

引数が一つの時には引数のカッコも省略できますが、複数だったり引数なしの場合は省略できません。

const increment = a => a + 1;

オブジェクトの作り方あれこれ

次にオブジェクトの作り方を確認してみます。

Objectクラス

一番基本の形です。(まぁリテラル使うのであんまり使わないですが…)
ここでのtypeとかnameはプロパティになります。

const obj = new Object();
obj.type = "猫";
obj.name = "たま";
console.log(obj);  // { type: '猫', name: 'たま' }

ちなみにプリミティブ型はオブジェクト扱いできませんが、関数型はオブジェクトなのでプロパティが普通に使えます。
余談ですが、ES6まではclassという文法自体がJavaScriptになかったのでfunctionを使ってクラスの表現をしていたのでまぁそういうことです。

const num = 3;
num.prop = "さん";
console.log(num.prop);  // undefined

const func = () => {};
num.prop = "かんすう";
console.log(func.prop);  // かんすう
オブジェクトリテラル

実はnew Object()の代わりに{}を使うと簡単にオブジェクトが作れます。

const obj = {};
obj.type = "猫";
obj.name = "たま";
console.log(obj);  // { type: '猫', name: 'たま' }

で、そのまま初期化もできちゃいます。ので、だいたいこっちを使いますね。

const obj = {
    type: "犬",
    name: "ぽち"
};
console.log(obj); // { type: '犬', name: 'ぽち' }

TypeScriptでもclassのコンストラクタを使って初期化とかせずに、型推論やasを使ってtypeやinterfaceに変換するとかありがちです。

type Animal = { 
    type: "猫" | "犬"; 
    name: string; 
};
const cat: Animal = {
    type: "猫",
    name: "たま"
};
const dog = {
    type: "犬",
    name: "ぽち"
} as Animal;

ラベル

めったに使わない機能としてラベルというものがあります。
goto構文を普通に使うような時代のものなので、現代のプログラミングにおいては積極的に使うべきものではないとは思います。
ちなみにJavaScriptにgotoはないので、breakとcontinueに使えます。
なんかこんな感じ。正直使わないのでよくわからないです…。

let cnt = 0;
outer: for (let i = 0; i < 100; i++) {
    inner: for (let j = 0; j < 100; j++) {
        cnt++;
        if (cnt % 10 === 0) {
            continue outer;
        }
    }
}
console.log(cnt); // 1000

breakだとこう。

let cnt = 0;
outer: for (let i = 0; i < 100; i++) {
    inner: for (let j = 0; j < 100; j++) {
        cnt++;
        if (cnt % 10 === 0) {
            break outer;
        }
    }
}
console.log(cnt); // 10

こんなこともできる。うれしくないけど。

hoge: {
    console.log(1);
    break hoge;
    console.log(2); // ここは呼ばれない
}
console.log(3);

で、なんで?

長くなってしまいましたが最初のコードに戻ります。

const func = () => { hoge: "fuga" };

これはアロー関数を使ってオブジェクトリテラルを返そうとしているコードです。
返却したい値が一つだけなのでカッコとreturnを省略しました。

が、残念なことにオブジェクトリテラルのカッコとアロー関数のカッコが同じ「{}」を利用しています。
そしてややこしいことにラベルという機能があるせいで「hoge: "fuga"」がエラーにならずに解釈することができます。
(これ単品でのラベルってどうやって使うんでしょうか…?正直よくわかりません…。)

するとなんとhogeというラベルを定義した謎の関数ができあがり、当然これは何の戻り値も返しません。(undefined)
なので、よく見るとIDEが「未使用のラベル」とかいう不思議な警告を出すわけです。

const func = () => { 
    hoge:    // 特に意味のないラベル
        "fuga"  // 特に意味のない通りすがりの文字列
    // なにもreturnしていない
};

というわけでこれはオブジェクトリテラルだよってことを明示するためにカッコをつけてあげます。

const func = () => ({ hoge: "fuga" });

整理すると難しいね

なんか思ったより長くなってしまった。
JavaScriptって融通が利くのでこういう不思議なことがたまに起こる気がします。
なんか知らんけどカッコつけとこうじゃなくてちゃんと理解できると次回から間違えなくて済みそうです。

それでは。

TypeScriptでLangChainを使ってみる その1 基礎編

こんばんは。
リボンライトの接触がおかしくなって部屋の片隅が妖しく赤く光っています。私です。

今回から何回かに渡ってLangChainを触っていこうかと思います。
LangChainはPython版とJavaScript(TypeScript)版があるのですが、Pythonの方が開発が先行しているためJS版は影が薄めです。
検索してもPythonのコードばかりがヒットしてとてもつらい。ので、自分で少しでも書こうかなと。

今回から触るコードはこのリポジトリで公開しています。
github.com

LangChainとは

LangChainはざっくりいうと大規模言語モデル(LLM)を使ったアプリケーションを簡単に作れるようにしたフレームワークです。
更新もとても活発で、迷ったらこれでいいとは思います。
www.langchain.com

ちなみにLangChain以外にもいくつかフレームワークがありますが、LlamaIndexとSemantic Kernelあたりが有名かなと思います。
www.llamaindex.ai
learn.microsoft.com

とりあえず動かしてみる(Model)

インストールするパッケージは詳しくはGitHubリポジトリを参照してください。
環境変数の利用のために「dotenv」、LangChain自体は「langchain」「@langchain/community」「@langchain/openai」「@langchain/google-genai」とかそのあたりです。
OpenAIやGeminiのAPIキーなどは.envファイルに記載して参照できるようにしておきましょう。

OPENAI_API_KEY="ほにゃらら"
GOOGLE_API_KEY="ふにゃらら"

まずはこんな感じで書くととりあえず簡単にチャットができます。モデルはたぶん「gpt-3.5-turbo」ですかね。

const chat = new ChatOpenAI({
    openAIApiKey: process.env.OPENAI_API_KEY
});

const result = await chat.invoke("こんばんは");
console.log(result);

結果はこんな感じ。

AIMessage {
  lc_kwargs: {
    content: 'こんばんは!今日はどのようなお話しをしましょうか?',
    additional_kwargs: { function_call: undefined, tool_calls: undefined }
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'こんばんは!今日はどのようなお話しをしましょうか?',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined }
}

実は環境変数の名前を「OPENAI_API_KEY」にしていると特にコンストラクタで何も渡さなくても勝手にそれを使ってくれます。
なので、これでも同じ結果になります。

const chat = new ChatOpenAI();

const result = await chat.invoke("こんばんは");
console.log(result);

で、便利なのがたったこれだけでGoogle Generative AIへのリクエストができてしまいます。たぶんモデルは「gemini-pro」。
いろいろ余計なものがついてるので、result.contentに変えてみました。

const chat = new ChatGoogleGenerativeAI();

const result = await chat.invoke("こんばんは");
console.log(result.content);

Geminiの方が詩的…なんですかね。

こんばんは。あなたの夜をより良くするために何かお手伝いできることはありますか?私はあなたとチャットしたり、ジョークを言ったり、質問に答えたりすることができます。

プロンプトを作ってみる(Prompt)

LLMに渡すプロンプトは固定の文字列ではなく、ユーザーの入力など何らかの変数を埋め込むことが良くあります。
そんな場合にはPromptTemplateの機能を使います。

const promptTemplate1 = new PromptTemplate({
    template: "今日は{day_of_week}、明日は?",
    inputVariables: [ "day_of_week" ]   // 変数の定義を書く
});
const prompt1 = await promptTemplate1.format({ day_of_week: "金曜日" });
console.log(prompt1);

// fromTemplateを使うと変数は勝手にいい感じにしてくれる
const promptTemplate2 = PromptTemplate.fromTemplate("今日は{day_of_week}、明日は?");
const prompt2 = await promptTemplate2.format({ day_of_week: "金曜日" });
console.log(prompt2);

チャットによる会話を作る場合にはChatPromptTemplateを使うと良いです。
ロールごとのPromptTemplateもあります。

const chatTemplate1 = ChatPromptTemplate.fromMessages([
    [ "system", "あなたは{role}です" ],
    [ "human", "好きな{kinds}は何ですか?" ]
]);
const prompt1 = await chatTemplate1.format({ role: "賢い猫", kinds: "食べ物" });
console.log(prompt1);

const chatTemplate2 = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("あなたは{role}です"),
    HumanMessagePromptTemplate.fromTemplate("好きな{kinds}は何ですか?")
]);
const prompt2 = await chatTemplate2.format({ role: "賢い猫", kinds: "食べ物" });
console.log(prompt2);

出力を制御してみる(OutputParser)

result.contentに変えないと邪魔なものがみたいなことを書きましたが、StringOutputParserを使うと結果の文字列だけを取り出すことができます。

const message = new AIMessage({
    content: 'こんばんは!なんかご用ですか?'
});

const outputParser = new StringOutputParser();
const result = await outputParser.invoke(message);
console.log(result);

CommaSeparatedListOutputParserを使うとカンマ区切りの文字列を配列で返してくれます。

const message = new AIMessage({
    content: '春,夏,秋,冬'
});

const outputParser = new CommaSeparatedListOutputParser();
const result = await outputParser.invoke(message);
console.log(result);

つなげてみよう(Chain)

ここまで作ったものをつなげてみます。

const chatTemplate = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("あなたは{role}です"),
    HumanMessagePromptTemplate.fromTemplate("好きな{kinds}は何ですか?")
]);
const prompt = await chatTemplate.format({ role: "賢い猫", kinds: "食べ物" });

const chat = new ChatGoogleGenerativeAI();
const chatResult = await chat.invoke(prompt);

const outputParser = new StringOutputParser();
const result = await outputParser.invoke(chatResult);

console.log(result);

いや…賢いけどちょっとイメージと違う…。

* **魚:** マグロ、サケ、タラ、ティラピア、イワシなど、ほとんどの猫は魚が大好きです。魚は猫にとって優れたタンパク質、オメガ3脂肪酸、その他の栄養素の供給源です。
* **鶏肉:** 鶏肉は、猫にとってもう一つの人気のあるタンパク質源です。鶏肉は調理しているか、生であり、味付けや調味料は含まれていないことを確認してください。
* **牛肉:** 牛肉は、猫が食べることができるもう一つのタンパク質源です。牛肉は調理しているか、生であり、味付けや調味料は含まれていないことを確認してください。
* **ラム肉:** ラム肉は、猫に人気のあるもう一つのタンパク質源です。ラム肉は調理しているか、生であり、味付けや調味料は含まれていないことを確認してください。
* **臓器肉:** 肝臓、心臓、腎臓などの臓器肉は、猫にとって優れたビタミン、ミネラル、その他の栄養素の供給源です。しかし、臓器肉は少量で与えてください。
* **卵:** 卵は、猫にとって優れたタンパク質、ビタミン、ミネラルの供給源です。卵は調理されているか、生であり、味付けや調味料は含まれていないことを確認してください。
* **乳製品:** 猫は一部の乳製品を食べることができますが、多くは乳糖不耐症です。猫に乳製品を与える場合は、牛乳よりもヨーグルトやケフィアなどの発酵乳製品を選ぶようにしましょう。
* **野菜:** 猫は一部の野菜を食べることができますが、ほとんどの猫は肉食動物です。猫に野菜を与える場合は、少量で、生よりも調理しているものを選ぶようにしましょう。
* **果物:** 猫は一部の果物を食べることができますが、ほとんどの猫は肉食動物です。猫に果物を与える場合は、少量で、砂糖が少ないものを選ぶようにしましょう。

実はpipe関数を使ってこんな風に書けます。まさにChainですね。

const chatTemplate = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("あなたは{role}です"),
    HumanMessagePromptTemplate.fromTemplate("好きな{kinds}は何ですか?")
]);
const chat = new ChatGoogleGenerativeAI();
const outputParser = new StringOutputParser();

const result = await chatTemplate.pipe(chat).pipe(outputParser).invoke({ role: "賢い猫", kinds: "食べ物" });

console.log(result);

で、そういうのってよく使うでしょってことで、LLMChainという名前であらかじめ用意されたりしています。
他にもいろいろなChainがあります。

const chatTemplate = ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate("あなたは{role}です"),
    HumanMessagePromptTemplate.fromTemplate("好きな{kinds}は何ですか?")
]);

const chat = new ChatGoogleGenerativeAI();
const outputParser = new StringOutputParser();

const chain = new LLMChain({
    prompt: chatTemplate,
    llm: chat,
    outputParser: outputParser
});

const result = await chain.invoke({ role: "賢い猫", kinds: "食べ物" });
console.log(result);

スマートだ

とりあえず今回は基本編ということでここまで。
ライブラリとしての出来が良くて参考になります。
LLMを使ったプログラムが簡単に書けるのは素敵ですね。

それでは。

ChromaDB+Dockerでお手軽にベクトルDBを使ってみよう

こんばんは。
本が読みたいけど首が痛いです。

RAGが作りたい

ChatGPTなどのLLMを利用して何かを作っているとRAG(Retrieval-Augmented Generation)という仕組みが作りたくなってきます。
RAGとは簡単に説明すると外部のデータを使ってLLMに情報を与えるパターンです。
とてもシンプルなプロンプトだと「以下のデータを参考に質問に答えてください。 参考:{context} 質問:{question}」みたいな感じですね。

その場合、基になる情報をどこかから引っ張ってくる必要があるのですが、RDBなどを使うと自然言語から条件にマッチする情報を取ってくるのはなかなか難しいです。
そんな場合にはベクトルDBによる検索が便利です。

ベクトルDBは保存しておく情報をベクトルに変換し、検索においても入力をベクトルに変換します。
そしてコサイン類似度などの手法を用いてベクトル同士の類似度を測り、意味的に近い情報を取得することができます。
似たような言葉でセマンティック検索というものがありますが、こちらは意味的に近い情報を取得する検索方法でベクトル検索に限ったものではないです。
なのでベクトル検索はセマンティック検索の一種と言ってもまぁ差し支えなさそうです。

ベクトル空間モデル - Wikipedia

ベクトルDBって

さて、RDBであればOracleSQLServerMySQLなどが思い浮かびますが、ベクトルDBというとぱっと思いつきません。
調べてみるとQdrant、FAISS、SaaSだけどPinecone、あとはElasticSearchでもできたりするみたいです。
が、なんだかあまりお手軽そうではないです。とりあえず使ってみたいのだけど…。

そんな中でChromaはだいぶお手軽に使えそうです。pipやnpmでインストールしてローカルで簡単に動くらしい。
でもまぁインメモリとかでやるのも何なので、今回はDockerでコンテナを建ててみます。
docs.trychroma.com

とてもシンプル

例によってDocker composeを使いますが、たったこれだけです。

version: '3.8'
services:
  chroma:
    image: chromadb/chroma
    container_name: chroma
    ports:
      - 8000:8000
    volumes:
      - ./chroma:/chroma/chroma

GitHubをのぞいてみるとPERSIST_DIRECTORY(永続化ディレクトリ)が「/chroma/chroma」なのでマウントしておくくらいですかね。
chroma/docker-compose.yml at main · chroma-core/chroma · GitHub

あとは普通にコンテナを立ち上げましょう。

> docker compose up -d

TypeScriptで試してみる

いつもの感じで適当にディレクトリを作って、とりあえず初期化と必要なものをインストール。

> npm init -y
> npm install typescript @types/node ts-node chromadb openai

index.tsを作ったらpackage.jsonを書き換えて準備はOK。

~略~
  "scripts": {
    "start": "ts-node index.ts"
  },
~略~

で、こんな感じ。ベクトル化にはOpenAIのAPIを使っていますが、チャットよりだいぶお安い感じなのでバシバシ使っても大した金額にはならないです。

import { ChromaClient, OpenAIEmbeddingFunction } from 'chromadb';

(async () => {

    const client = new ChromaClient({
        path: "http://docker-server-address:8000"
    });

    const embedding = new OpenAIEmbeddingFunction({
        openai_api_key: "your_api_key",
    });

    const collection = await client.createCollection({
        name: "sample",
        embeddingFunction: embedding
    });

    await collection.add({
        ids: ["id1", "id2", "id3", "id4"],
        metadatas: [{ source: "sample" }, { source: "sample" }, { source: "sample" }, { source: "sample" }],
        documents: ["とっても眠い", "猫がとてもかわいい", "犬もとてもかわいい", "お寿司が食べたい"],
    });

    const results = await collection.query({
        nResults: 2,
        queryTexts: ["ネコカワイイ"],
    });
    console.log(results);

})();

実行してみるとこんな感じ。それっぽいですね。

> npm start

> chroma_test@1.0.0 start
> ts-node index.ts

{
  ids: [ [ 'id2', 'id3' ] ],
  distances: [ [ 0.17901252107447224, 0.26748711167475037 ] ],
  metadatas: [ [ [Object], [Object] ] ],
  embeddings: null,
  documents: [ [ '猫がとてもかわいい', '犬もとてもかわいい' ] ],
  uris: null,
  data: null
}

そんなこんなで割と簡単にベクトル検索ができました。

で、RAGは

はい。次回以降ということでね。
ちなみにそろそろ生のクライアントを使うのがつらくなってきたので、ついでにLangChainについても学んでみようかなと。
Webで検索するとPythonばかりでJavaScript(TypeScript)の情報が全然出てこないので、そのあたりも確認しつつ。