MLTensor は行列計算の関数が充実している

SwiftでRAG実装 Part 2:クエリに類似するドキュメント検索の試み で実装していて気がついたのだが、MLTensor の API ドキュメントを眺めると行列計算の関数が充実しることを知って感動した。

MLTensor | Apple Developer Documentation

昔、SCNVector 同士の計算を実装しようとした時、ベクトルという名でありながらベクトル計算の API がまったくなかったので残念に思ったことがあったのでなおさら。

どんな関数があるかというと、行列とスカラの演算はもちろん、行列同士の内積、軸ごとの最大/平均/最小を求めたり、軸に沿って累積積を計算したり、1次元に再配列したり、とキリがない。どんなアウトプットになるのか試してみないとわからないものも多い。

ただ、Core ML のコンテクストで引き回すデータ表現としては MLTensor が相性良いものの、行列演算そのものの高速性を求めるなら、やはり Accelerate framework を使ったほうが良いらしい(ChatGPTによる)。

iPhone 17 Pro を購入した

iPhone 15 Pro から2年振り。今回はカメラ進化が大きく、写真撮影がメインの筆者にとっては買い替えは必須。色はディープブルー、容量は256GB。(iCloudの契約ストレージは5TB、、)

本当はおととい19日に手に入る予定だったが、帰省予定で実家宛に送っており、コロナ罹患が分かった時には配送先が変えられず、実家から転送してもらい受け取れたのが今日だった。

筐体はエッジが丸みを帯びていて、iPhone 15 Pro よりもアールが緩やかで手に馴染み、梨地仕上げのアルミ素材が背面まで回り込んでいるので手触りも優しいため、ケースをつけずに使っていたい気持ちになる。が、近い将来絶対に落とすし、いくら Ceramic Shield 2 とはいえ過度な信頼はできないので、ケースは必ず着ける。

ケースははじめ TechWoven のパープルを予約していたが、後から考え直してシリコンのオレンジを再注文した。商品ページではディープブルーの藍色とオレンジの明るさとの落差がおおきく一度は見送ったのだが、これまでも茶色(トープ)の FineWoven を使っていて、シックな色にも飽きたのが返品理由。あと、シンプルにウーブン系の素材に懲りたというのも、、TechWoven は改良されているに違いないが。

iPhone 17 Pro のディープブルーは、光の当たり方によっては意外と色味鮮やに映るので、オレンジのような彩度高い色との相性も問題ないと感じた。

筐体はディープブルーでありながら、新色コズミックオレンジ感も味わえてお得な組み合わせ。ジェネリック・コズミックオレンジ。

カメラもいくつか撮影して試してみたが、特に8倍の光学(品質の)ズームに関しては、今まで寄れなかった遠くのものまで鮮明に写せるのが嬉しいし、遠景前景との圧縮効果を生み出すこともできるので、これまでの iPhone ではかなわなかった構図を楽しめることが分かった。(8倍は実質4倍の部分拡大なので、「圧縮効果」というと語弊があることは承知、、)

デジタルズーム 40 倍だと、3キロ離れたオフィスビルもここまで映し出せる。

コロナに罹ってしまった

連休明けから喉が痛くて警戒していたが、案の定午後から発熱。翌日病院に行き抗原検査を受けたらあっさりコロナ認定されて頭が真っ白になった。週末が誕生日というのもあり盆休みからずらして5連休にし、帰省の予定を立てたり、いくつか人と会う約束もしていたのだが、、しょうがないので今年の誕生日は悪あがきせず、おとなしく家で過ごすことになった。

さらに不幸なことに、iPhone 17 Pro を日程的に実家で受け取ろうと配送先指定していたのだが、コロナ感染が判明したタイミングで配送先を変更することができなかった。ちなみに同時注文したケースは東京の自宅に届いたのだが、注文翌日に配送先変更したのが間に合わなかったのだろう。もしかすると本体も自宅に届くのではと、一縷の望みを抱きつつ荷物追跡をチェックしていたが、指定通りの実家へと飛び去っていくのをただ見守ることしかできなかった。

そんな今朝、1ヶ月前に注文した、4-way の教則本がようやく届いた。これを誕生日プレゼント代わりにパラパラめくることにする。ちなみに感染後3日経ったが、熱はおさまって身体は多少楽になった一方で、逆にウイルスが気管支に回ったから咳や鼻が徐々にひどくなっているし、全身の痛みも出てきたので、練習はとてもできる状態ではない。

4-way Coordination by Marvin Dahlgren and Elliot Fine

SwiftでRAG実装 Part 2:クエリに類似するドキュメント検索の試み

SwiftでRAG実装 Part 1:テキストをコンテキストベクトル化 の続き。

前回はドキュメントデータから埋め込み生成。今回はそこから検索クエリに類似するドキュメントを抽出してみる。先に結論を言うと、結果はいまひとつなので調整が必要そう。

今回実装する計算ロジックは以下(現状の筆者の理解であることに注意)。

  1. ドキュメントからそれぞれ埋め込み生成し、ベクトル化する
  2. ドキュメントごとのベクトル表現(D次元)を、ドキュメントの数(M個)分並べて MxD 行列を生成する(ドキュメント行列)
  3. クエリ文字列をベクトル化する。1. と同じ手法で生成しベクトル次元は一致(D次元)。(クエリベクトル)
  4. ドキュメント行列・クエリベクトル(内積)を計算する (MxD・Dx1 = Mx1)
    • つまりドキュメントごとのベクトルとクエリベクトルとの内積が結果として求まる。内積ベクトルのノルム(コサイン類似度)が1に近いほど類似度高く、0、-1に近づくほど類似度が低いと判定できる
  5. 4. の結果で得られた類似度の配列(要素数M)を降順にソート、上から任意の件数を上位ヒットとする
var docTensors: [MLTensor] = ...

// docTensors に含むドキュメントごとのクエリとの類似度を検索し、上位任意件数(maxCount)をヒット結果として返す
// (返すのは該当文書テンソルの docTensors 配列内におけるインデックス番号)
func search(query: String, maxCount: Int) async -> [Int] {
    // ドキュメントを集積した MxD 行列
    let flatteneds = tensors.map { $0.flattened() }
    let docsMat = MLTensor(stacking: flatteneds)
    // クエリベクトル Mx1 (encode関数は前回記事参照)
    guard let queryVec = try? embedder.encode(text: query, asColumn: true /*列ベクトルで出力*/) else { return [] }
    
    // ドキュメント行列・クエリベクトル (MxD・Dx1 = Mx1)
    let mulResult = docsMat.matmul(queryTensor)
    // 類似度スコア配列に変換
    let calcScores = await mulResult.shapedArray(of: Float.self).scalars
    // ソートして、トップの結果をインデックス番号として抽出
    let map = zip(Array(0..<docTensors.count), calcScores)
    let sorted = map.sorted { $0.1 > $1.1 }.prefix(maxCount)
    return sorted.map { $0.0 }
}

これでサンプルの日記データを検索したところ、肌感としてあまり精度高くなかった。

ただし、クエリの何かしらの特徴は反映していそうだった。たとえば「I watched a movie」のクエリで検索すると、映画を見た日記エントリはヒットしないが、上位ヒットは軒並み「I (動詞過去形)」のパターンで始まっていたり、更に分かりやすい例を挙げると、次のように「at a cafe」クエリに対し「At a/the」で始まるエントリが抽出されたりした。まったくの出鱈目ではなさそうだが、肝心の文章的なコンテキストは明らかに落ちているように見える。

Screenshot

追記(2025/10/01):後日性能改善できたので後々投稿するが、取り急ぎこちらの資料にソースコードを添付した。(enumerateTokenVectors を使うようにしただけ)

参加メモ:DroidKaigi 2025

会場エントランス

昨日までの二日間、渋谷で開催された DroidKaigi 2025 に参加してきた。

DroidKaigi 2025

スタッフとして知り合いが複数人参加していたり、会場にも顔見知りが多くいた関係で、常に誰かと話しているような状況だった。最後話したのがいつだったかも思い出せないくらい、久しぶりに顔を見た新卒時代の同僚とも、たくさん話すことができた。
何より同僚の登壇もあったので、最前列で応援することができて良かった。

iOSDC や try! Swift とはまた違った雰囲気だったのも印象的だった。スポンサーブースはその出展数の多さから祭りのように賑やかだった。その数40越え。スポンサースタンプラリー完走はほぼ不可能のように思えた。

他にもアイスがタダでもらえたり、バリスタコーヒーも選択肢が豊富だったり、ネイルが体験できるコーナーがあったりと文化祭感。発表40分、休憩20分と計1時間が交互に繰り返される点も、キレ良くて分かりやすかった。また、国内カンファレンスの割に外国人比率が高めだったのも意外だった。

アフターパーティではマグロの解体ショーも

聴講したセッションについてはまだ内容を咀嚼しきれておらず、きちんと復習したい。ありがたいことに、ほぼ即日で全セッションの動画が配信されていた。

DroidKaigi 2025 – YouTube

個人的に印象に残ったセッション3選。


最後に、初体験だったネイルの写真。

SwiftでRAG実装 Part 1:テキストをコンテキストベクトル化

Foundation Models のツール呼び出しに使う RAG に Spotlight を利用する試みが絶たれたので、自前で RAG 実装することにした。(本記事の作業は↓のワークショップで取り組んだもの)

経緯:参加メモ:新しいFoundation Modelフレームワークのアプリへの導入(ワークショップ)@Apple Japan

このブログでは繰り返し何度も貼っているが、Apple 武石さんのこのポストを頼みの綱に頑張る。もはやバイブル。


といってもちんぷんかんぷんなので ChatGPT に聞いてみた。

NLContextualEmbedding を使ってRAGを構築するにはどう言った実装が必要になりますか?

NLContextualEmbedding を使った “自前RAG” の実装は、だいたい次の4層で考えると設計しやすいです。

  1. 前処理(分割・メタデータ付与)
  2. 埋め込み生成(NLContextualEmbedding)
  3. ベクタ格納と近傍検索(SQLite + BLOB / CoreData / なんでも)
  4. 検索→再ランキング→プロンプト組み立て→生成(Foundation Models)

いったん前処理は省いて、まずは短い文字列を元にした埋め込み生成(コンテキストベクトル化)を試みた。ChatGPT の実装をもとに動くよう修正し、最終的にそれっぽくベクトルを得ることができた。(コメントは筆者理解の補足なので間違いあるかも)(2025/10/5 実装修正)

import Accelerate
import NaturalLanguage

let embedding: NLContextualEmbedding

/// 文字列をコンテキストベクトル化(平均プーリング+L2正規化)
func encode(text: String) throws -> [Float] {
    let result = try embedding.embeddingResult(for: text, language: overrideLanguage)

    let dim = embedding.dimension
    var sum = [Float](repeating: 0, count: dim)
    var count = 0 // トークン数

    result.enumerateTokenVectors(in: text.startIndex..<text.endIndex) { vec_double, range in
        var vec_float = [Float](repeating: 0, count: dim)
        // double → float 変換
        vDSP_vdpsp(vec_double, 1, &vec_float, 1, vDSP_Length(dim))
        // vec_float を sum に足し合わせ
        vDSP_vadd(sum, 1, vec_float, 1, &sum, 1, vDSP_Length(dim))
        // トークン数をインクリメント
        count += 1
        return true
    }

    guard count > 0 else { return [Float](repeating: 0, count: dim) }

    // 平均プーリング(トークンベクトルの総和をトークン数で平均して畳み込み)
    var inv_n = 1.0 / Float(count)
    vDSP_vsmul(sum, 1, &inv_n, &sum, 1, vDSP_Length(dim))

    return l2Normalize(sum) // L2 normalization
}

// L2 正規化(ベクトル全体を二乗和平方根で割って正規化)
private func l2Normalize(_ v: [Float]) -> [Float] {
    var vec = v
    var norm: Float = 0
    vDSP_svesq(vec, 1, &norm, vDSP_Length(vec.count))
    norm = sqrtf(norm) + 1e-12
    vDSP_vsdiv(vec, 1, &norm, &vec, 1, vDSP_Length(vec.count))
    return vec
}
print(encode(text: "Hello, world.")

[-0.028537132, -0.014218736, -0.033890422, -0.024530113, 0.009770119, -0.01361734, 0.0034657633, 0.029605899, 0.013323085, -0.005046577, ..., 0.018509272, -0.026693422, -0.6423329, -0.03437927, 0.005926335, -0.022124525, 0.03561643, -0.056179043, 0.025543895, -0.00908023, 0.0050482955, 0.028503625]

ちなみに、今回 “Hello, world.” は Hello,, , world. の3トークンに分割された。


埋め込み生成の処理をおさらいすると

  1. 文字列をもとにベクトル埋め込みを生成(NaturalLanguage.NLContextualEmbedding.embeddingResult(for:language:)
  2. 文字列をトークン(サブテキスト単位)に分割
  3. トークンごとに、トークンベクトルを抽出(NaturalLanguage.NLContextualEmbeddingResult.tokenVector(at:)
  4. すべてのトークンベクトルの平均を計算 → コンテクストベクトル
  5. コンテクストベクトルを二乗和平方根で割って正規化

これ書きながら、2-3 のステップで頑張ってループ回しているところは enumerateTokenVectors(in:using:) 使ったほう良いかも、と思った。

参考:
埋め込み層 (Embedding Layer) [自然言語処理の文脈で] | CVMLエキスパートガイド
平均プーリング(Average Pooling) | CVMLエキスパートガイド
[iOS 17] 多言語BERT埋め込みモデルのサポート


Accelerate framework に馴染みがないので、ここで使われている関数を調べてみた。

  • vDSP_vdpsp: 倍精度のベクトルを単精度のベクトルに変換
  • vDSP_vadd: ベクトル同士の和 (stride 指定可能)
    • stride: 足し合わせ時の要素飛び石数 通常は1だが、オーディオバッファからLRチャンネルを分離して取得する時(stride=2)や、イメージバッファからRGBチャンネルを分離して取得する(stride=3)時に指定
  • vDSP_vsmul: ベクトル * スカラ値 の積算 (stride 指定可能)
  • vDSP_vsdiv: ベクトル / スカラ値 の除算 (stride 指定可能)
  • vDSP_svesq: ベクトルの二乗和 sum(a^2) 、結果はスカラ値

Apple “Awe Dropping” イベント雑感

朝1:45起床。WWDC 25 と同様に、Vision Pro をかぶってリアルタイムに視聴した。

Apple Vision Pro で湖畔越しに

だいたいリーク通りだったので驚きはなかった。しょっぱな AirPods Pro 3 は、イントロビデオのラストを見て、一瞬ブラックモデルが出るのかと思ったが、なかった。でも同じようなことを思った人は他にもいた。

ライブ翻訳機能は Pro 3 に限らず以前の一部モデルでも使えるようになるようだ。近い未来、AirPods 越しに外国語話者と会話するのが当たり前となる時代が来るかもしれない。


iPhone については 17 Pro で今回大幅にカメラアップデートがあったので 15 Pro から買い換え予定。Air も魅力的だったが、カメラ性能を最重視する身としては背に腹が代えられなかった。

iPhone Air と iPhone Pro 17では、「カメラバンプ」内にカメラモジュールだけでなくチップなどあらゆるコンポーネントを詰め込む設計となっているが、これを Apple は “(camera) plateau”(プラトー)と名付けたようだ。この見慣れない単語は「高原・台地」を意味するらしい。iPhone に、Dynamic Island に次ぐ、あらたな地名が誕生した。(ちなみに日本語字幕では「凸部」と訳されていた)そしてこの設計をフォルダブルへの布石と見た。

The new “plateau” / A screenshot from the Apple event

ちなみに、イベント全体を通してもっともテンションが上がったのは、Apple Watch Hermès モデルへの文字盤追加(Faubourg Party)の発表だった。Apple Watch Hermès ユーザーとしては毎度期待するところ。3年前に発表された Luckey Horse 以来の大胆なデザインで、今回も楽しみ。

Hermès Faubourg Party watch face / A screenshot from the Apple event

最後に、今回の夜のお供。

明治マカダミアチョコとオーザック

参加メモ:新しいFoundation Modelフレームワークのアプリへの導入(ワークショップ)@Apple Japan

アップルジャパンで開かれた Foundation Models のワークショップに参加してきた。結果から言うと、知りたいこと、気になっていたことはすべて明快にでき、お話もコーディングもたくさんできて、とても充実した時間となった。

参加者が少なかったおかげで、心置きなくテクノロジーエヴァンジェリスト武石さんとお話しできたのが良かった。Foundation Models に限らず、何をお尋ねしても的確な回答をくださったのがとても心強く、今後の取り組み方にも自信を持つことができた。自学自習だけだとこうはいかない。

ワークショップ:新しいFoundation Modelフレームワークのアプリへの導入

ワークショップとはいうものの、中身は事前に告知されていた通りにもくもく会で、冒頭5〜10分 Foundation Models framework に関するイントロがあったのち、3時間ほぼほぼ質問と作業時間に費やすことができた。

このブログで長々したためている通り、筆者は「日記を Spotlight にインデクスし Foundation Models に検索させ、対話に使う」ことをテーマに取り組んでおり、そのプロジェクトを持ち込んだ。課題が山積していて、相談はおおきく以下があった。

参考:メインで対応くださった武石さんのツイート


冒頭、いくつか tips の紹介があった。(メモしている範囲で)

  • Languages
    • デフォルトのレスポンス言語は、入力(プロンプト)の言語によって決定される
      • tool calling に英語が混ざっていたら英語になる可能性があるので、instruction で言語を指示すると良い
    • プロンプトには LocalizedStringLocale.current を instruction 指定に使うのを推奨(ユーザーの言語環境に応じられる)
  • Tool calling
    • ツールを強制的に呼び出す際は、instruction に強い表現を使うこと(プロンプトテクニック)
      • ALWAYS USE *** tool to find…
      • You MUST USE *** tool
  • Handling limited token
    • 4096 token というトークン上限
      • 日本語は、CJK は1文字1token(英語は3-4文字1token)
    • トークン消費を見るためのAPIはない
      • やりとりの他にも、生成された結果もセッション内でトークンに含めて管理しているのでややこしい
      • exeededContextWindowSize エラーを捕捉して新しいセッションを作る
      • GenerationOptions.maximumResponseTokens でコントロールする
      • 長い文章を扱う場合、NLTokenizer でチャンクを作る(Natural Language framework)、パラグラフベースで文章を切ることが可能

質疑で新たに知ることができたことのメモ。事前に実験したこと、考えていたことをもとにディスカッションくださった。

  • Core Spotlight のセマンティック検索挙動について
    • 部分一致的な挙動になるのが現状の性能限界
    • Siri の改善が追いついていない
    • WWDC24 セッションで言及されたサンプルコードも現状存在しない
  • ツール呼び出しをしてくれないケースがある
    • 指示文に “must use the tool (“SearchDiaryTool”)” と書いていたところを、よりシンプルに “must use the SearchDiaryTool” とするだけで変わるかも
    • 肌感としてツールの名称定義を、動詞-名詞 (SearchDiary, GetXXX) のようにするなどすると結果が変わったりする
    • ↑ など、指示文の細かい書き方のチューニングが効果ある
    • (ツール呼び出しに限らず)日本語よりも英語の方がパフォーマンスが良い
  • ツール呼び出しの制限
    • タイムアウトは特に明言されていなく、ない認識
    • だが、ツール呼び出し周りの情報がトークンウィンドウを溢れさせて終了することはある
      • セッション自体の指示文やプロンプトだけでなく、ツール定義(指示文など)もウィンドウを占有する。複数ツールを与えるとその分オーバーヘッドが増大する
      • ツール呼び出しでツールから取得した情報もすべて、最終的なレスポンスに関わらずトークンウィンドウを占有する
    • Foundation Models にツール呼び出しをさせるよりも、Foundation Models 外で情報を事前に取得し、プロンプトとして与える方が良い(ツール呼び出しの不確実性 < プロンプトで与えた情報が使われる確実性)
      • Foundation Models は自然言語的に回答を整形するのに使うという割り切りも検討
  • RAG 実装
    • ↑ 武石さんツイートの技術的な噛み砕きや、サンプルコードをもとにした解説
    • チャンク化→ベクトル化したものを、メタデータ付きの構造体としてラップしてあげ照合することで、より豊かな検索体験が作れそう
    • ベクトル化した情報は必ずしも SQL や CoreData に永続化する必要はない(規模によってはオンメモリでも十分)
  • そのほか
    • Foundation Models の得意分野として、結局、サマリや抽出が得意
      • Writing Tools も内部的に同じモデルを使っている
      • Writing Tools では、RAGの自前実装で行わなければいけないチャンク化など内部的に勝手にやってくれるのでラク

前半は上記の質疑を中心に、後半は RAG の実装にチャレンジしていて、つまりどころはその都度質問させていただいた。その際、Apple 社内で流通しているサンプルコードを参考に見せてくださったのは、実現方法が一目瞭然でとても嬉しかった。

ということで今日いただいた情報をもとに、Core Spotlight にはいったん見切りをつけて RAG を自前実装する方向に舵きりしたいと思う。

聴講メモ:集まれSwift好き!Swift愛好会 vol.95 @ 株式会社スマートバンク

オフライン参加のつもりが予定が狂い、今回はオンライン視聴。ラストの Liquid Glass 対応に関するトークはオフライン限定ということで、残念ながら聴講できず。最後のアセットコンパイルの話で、Assets に含めた svg ファイルが、ビルドの過程で各解像度の png に変換されていると知り目から鱗。

イベントページ:https://love-swift.connpass.com/event/367730/
アーカイブ動画:https://youtube.com/live/uZpxIPIg6F0


アプリの “かわいい” を支えるアニメーションツールRiveについて

上ちょ / uetyo さん

  • Rive とは? “インタラクティブ” アニメーションツール
  • ユーザーのタップを起点にアニメーション、音声再生、Haptic も可能
  • 複数種類のアニメーションを用意したい場合
    • Lottie アニメーションは種類分の個数が必要だが、Rive はファイルは一つのみでOK
    • Rive ファイル内で分岐が完結
  • RiveViewModel: Rive ファイルに指示するプロトコルを SDK が提供(型安全)
  • riveViewModel.view() でビュー生成
  • Screen に手を加える必要なし
  • アニメーション、再生タイミング、タイマー調整… は従来、エンジニア実装、デザイナー確認のラリーが発生していたが、 Rive ファイル内でデザイナー完結するのでラク
  • アニメーションの制作方法:
    • 初期:Rive ファイルの作成をエンジニアが主導+デザイナー巻き込み
    • 現在:デザイナーで完結
    • Rive は Duolingo が最も利用している、アニメータ専門チームが存在
  • Lottie Animation との使い分け
    • Rive:パフォーマンスが高い(メモリ使用量、CPUのレンダリングコスト)
    • 銀の弾丸ではない、ループ再生は Lottie を公式が推奨する
    • カスタムフォントはキャッシュ機構が必要(メモリ使用が増大し不安定に)
  • Q&A
    • Lottie と比べて微妙な点
      • Rive が高機能すぎて作りにくい
      • Lottie は AE から直接エクスポートできるが、Rive は専用のツール (Rive Editor) が必要で学習うコストが高い
      • 前述のパフォーマンスに対する考慮が必要
    • 後発の Rive だからこそ導入時の困りごと
      • トラブル時情報が少ない(フォーラムやGitHub Issueで質問→レスポンスに時間がかかる)
      • 日本語での情報が少ない、AIの学習も進んでいない
    • 採用すべき
      • ただのローディングだけであれば慣れ親しんだLottieが良さそう
      • 画面全体で利用したい、アニメーション途中のインタラクションをさせたいケースはおすすめ
    • 両方採用はあり?
      • ワンバンクは両方利用
      • ケースによって片方に倒すのもあり

異業種からiOSエンジニアへの道のり〜iOS開発の魅力〜

hinakko さん

“聴講メモ:集まれSwift好き!Swift愛好会 vol.95 @ 株式会社スマートバンク” の続きを読む

SwiftUI:.searchable でキーボードのエンターが押された時の処理

キーボードでエンターキーが押された時の処理実装においては .onSubmit { ... } modifier を使いイベント検知できるが、.searchable modifier による検索フィールドに対しては検索キーのタップイベントが捕捉できなかった。

.onSubmit には、実は隠れたパラメタ trigger: SubmitTriggers がある。デフォルトで .text が指定されており、入力補完で省略されると気がつけない。

onSubmit(of:_:) | Apple Developer Documentation

以下のように、trigger: .search と指定することで解決できた。

CustomScreen()
   .searchable(text: $text)
// .onSubmit {
//       ❌ 呼ばれない           
// }
   .onSubmit(of: .search, {
      // 👍 呼ばれる
   }