エイエイレトリック

なぐりがき

# 詳しくない言語の発音をローマ字表記にする + epitranの紹介

「日本語(というか漢字)は表意文字なので読めなくても意味はわかる。表音文字は意味はわからなくても読める」 という話がある。 前者の主張はわかるが、正直なところラテン文字以外は発音を推定することは難しい(私の場合)。

そういうわけで、色々な言語をローマ字に変換する仕組みを考える。

言語ごと直接変換するのはコストが高いため、まずは文字を 国際音声記号(IPA) に変換した上でローマ字に変換する。

国際音声記号 (IPA) に変換

国際音声記号(IPA) に変換するのには epitran を使う。 多言語対応のライブラリは Python だとこれ一択の模様。

研究成果として公開されたもののようだが (https://aclanthology.org/L18-1429/ ) 、今もメンテナンスされている。ありがたい。

言語のサポート状況は README に記載されている。 使いたい言語 (Code) を指定して Epitran クラスで宣言する。

import epitran

epi = epitran.Epitran("code")

日本語を発音記号に

日本語対応もしているので試しに実行する。

日本語は Code が複数ある。 ひらがな (jpn-Hira)、カタカナ (jpn-Kana) に関してはそれぞれの文字種のみ変換する。

>>> epi = epitran.Epitran("jpn-Hira")
>>> epi.transliterate("とうきょうと、トウキョウ、東京")
'toːkʲoːto、トウキョウ、東京'

>>> epi = epitran.Epitran("jpn-Kana")
>>> epi.transliterate("とうきょうと、トウキョウ、東京")
'とうきょうと、toːkʲoː、東京'
>>> epi = epitran.Epitran("jpn-Jpan")

漢字を含む jpn-Jpan に関しては open-dict-data の ja.txt を辞書データとして使っている。

ひらがな・カタカナ部分について微妙に出力が変わる。 また、辞書ベースの仕組みのため変換できない可能性がある。

>>> epi = epitran.Epitran("jpn-Jpan")
>>> epi.transliterate("とうきょうと、トウキョウ、東京")
'toɯきょɯto、toɯkiョɯ、toɯkjoɯ'

>>> epi.transliterate("私は夢洲に行った")
'ɰᵝataihajɯmeɕɯɯnigjoɯっta'

>>> epi.transliterate("大規模言語モデル、つまりLLMのことだ")
'daikibogeɴkjomodeɾɯ、tsɯmaりLLMnokotoda'

ロシア語をローマ字っぽくする

ここから本題。ギリギリ読めないキリル文字で実験する。

ロシアの地名を Wikipedia ページ Список городов России с населением более 100 тысяч человек (人口10万人を超えるロシアの都市の一覧) のテーブルから取得。

日本語表記を正解データとして扱えるよう、 langlinks で対応する日本語ページも取得する。 日本語がない地名は落として、 156個 のデータを取得した。

// ロシア語地名と日本語
[
    {
        'target': 'Москва',
        'ja': 'モスクワ'
    },
    {
        'target': 'Санкт-Петербург',
        'ja': 'サンクトペテルブルク'
    },
    {
        'target': 'Новосибирск',
        'ja': 'ノヴォシビルスク'
    },
    {
        'target': 'Екатеринбург',
        'ja': 'エカテリンブルク'
    },
]

MediaWiki API にリクエストし、 その結果を wikitextparser で色々操作しただけなので実装の詳細は割愛。

発音記号をローマ字に変換

epitran の変換結果をローマ字に変換する。

国際音声記号の文字一覧 を参考にルールベースで変換する。

変換を単純化するために母音と子音以外は変換の対象から除外する実装とした。

ipa_to_roman の実装は gist にアップロードした。

gist.github.com

検証

日本語カタカナもローマ字に変換した上でコサイン類似度を算出する。

カタカナからローマ字の変換は jaconv、 コサイン類似度は textdistance を使う。

def katakana2roman(text):
    """カタカナをローマ字に変換"""
    text = jaconv.kata2hira(text)
    # ゔ を v に
    text = text.replace("ゔぁ", "va").replace("ゔぃ", "vi").replace("ゔぅ", "vu")
    text = text.replace("ゔぇ", "ve").replace("ゔぉ", "vo").replace("ゔ", "v")
    # 長音・記号を空白に
    return jaconv.kana2alphabet(text).replace("ー", " ").replace("・", " ")

epi_ru = epitran.Epitran("rus-Cyrl")

sim_result = []

for d in data["data"]:
    # カッコははぶく
    ru_text = d["target"].split(" (")[0]
    ja_text = d["ja"].split(" (")[0]
    ru_ipa = epi_ru.transliterate(ru_text)
    # ja_ipa = epi_ja.transliterate(ja_text)
    ja_roman = katakana2roman(ja_text)
    ru_roman = ipa_to_roman(ru_ipa)
    sim = textdistance.cosine(ru_roman, ja_roman)
    sim_result.append([ru_text, ja_text, ru_roman, ja_roman, sim])

結果は以下のようになった。 ru_roman がロシア語のローマ字表記、 ja_roman が日本語のローマ字表記。

ru ja ru_roman ja_roman similarity
0 Москва モスクワ moskva mosukuwa 0.721688
1 Санкт-Петербург サンクトペテルブルク sankt-petervurɡ sankutopeteruburuku 0.710819
2 Новосибирск ノヴォシビルスク novosivirsk novoshibirusuku 0.778499
3 Екатеринбург エカテリンブルク yekaterinvurɡ ekaterinburuku 0.741249
4 Казань カザン kazan kazan 1

類似度 (similarity) の平均は 0.767622 でそこまで低くない印象。

# similarity (pd.describe)
count    156.000000
mean       0.767622
std        0.112924
min        0.462910
25%        0.683370
50%        0.771517
75%        0.843744
max        1.000000

実例

類似度の高い上位10個

ru ja ru_roman ja_roman similarity
8 Уфа ウファ ufa ufa 1
38 Пенза ペンザ penza penza 1
32 Кемерово ケメロヴォ kemerovo kemerovo 1
4 Казань カザン kazan kazan 1
51 Иваново イヴァノヴォ ivanovo ivanovo 1
137 Раменское ラメンスコエ ramenskoe ramensukoe 0.948683
150 Камышин カムイシン kamishin kamuishin 0.942809
125 Коломна コロムナ koromna koromuna 0.935414
115 Салават サラヴァト saravat saravato 0.935414
13 Воронеж ヴォロネジ voronej voroneji 0.935414

6文字ぐらいの単語でも類似度が高く表記できている。

類似度の低い上位10個

ru ja ru_roman ja_roman similarity
76 Тамбов タンボフ tamvov tanbofu 0.46291
99 Бийск ビイスク viysk biisuku 0.507093
113 Керчь ケルチ kerty keruchi 0.507093
86 Братск ブラーツク vratxk bura tsuku 0.516398
133 Рубцовск ルプツォフスク ruvtxovsk ruputsuofusuku 0.534522
126 Кызыл クズル kizir kuzuru 0.547723
140 Черкесск チェルケスク tyerkessk chierukesuku 0.57735
85 Великий Новгород ノヴゴロド verikiy novɡorod novgorodo 0.583333
66 Владикавказ ウラジカフカス vradikavkaz urajikafukasu 0.585369
144 Артём アルチョーム artom arucho mu 0.596285

日本語ではバ行表記 (b) 、原語だと v 表記の場合は類似度が低くなってしまう。 他は母音の不一致が多い。

Кызыл の変換結果 kizirクズル は母音が全く違うので詳しく調べてみる。

ロシア語での発音は kɨzɨl で母音 ɨ非円唇中舌狭母音。 日本語では表記できない発音らしい。

このあたりは日本語の限界を感じる。 音で聞くと「クィズィル」みたいに聞こえた。

参考

eieito.hatenablog.com

# Google Street View から埋め込み用のURLを取得するブックマークレット

ここ1年ぐらい Geoguessr というゲームをプレイしている。

Google Street View から、位置を予測するゲーム。 なので、ゲームの結果画面からStreet Viewのリンクに飛んで、実際の情報を確認することができる。

URLリンクを個人の notion に埋め込んで、復習に使っている。

この埋め込み用のリンク、取得が地味に面倒。 以下のように何ステップか挟む。

  • 「画面を共有または埋め込む」をクリック
  • 「地図を埋め込む」のHTML (iframe) をコピーする
  • iframe から埋め込みリンク (src="https://google.com/maps/embed?....") 部分だけコピー (notionではHTMLタグは不要なため)
  • notion ページにペースト

そこで、Google Maps Platform の Maps Embed APIを使って、ワンクリックで埋め込み用のURLを取得する仕組みを考えた。

JavaScript で書いて、ブックマークレットとして使えるようにした。 個人で使う目的で、サーバーを建てるのも面倒なので。

コードは以下の通り。

Google Street View から埋め込み用のURLを取得するブックマークレット · GitHub

chatGPT にサクッと作らせるつもりが結構手間取ったのでまとめる。

準備

Google Cloud の Google Maps Platform で Maps Embed API が使用可能な APIキーを作成する。

デジタル署名などの設定もあるが、 今回は私用利用で、privateなnotionページ上で利用するだけなので、特に設定していない。

Maps Embed API

Street view を埋め込むためのエンドポイントは https://www.google.com/maps/embed/v1/streetview

パラメーターには位置情報 (location など) と API key を必要とする。

API の設定は必要なものの、使用料金は無料 *1。 安心して活用できる。

Street view から位置情報を取得

位置情報がどう設定されているかについて、Google Map公式の情報がない。 以降は非公式情報に基づいている。情報ソースは末尾のリンクに列挙。

(Street Viewで実際に位置を動かしてみれば分かる仕様なので、問題ないはず)

カムチャッカ半島にある富士山みたいな山 (たぶん クリュチェフスカヤ山) が望める場所を例に見る。

ブラウザに表示される時のURLは以下の通り。

https://www.google.com/maps/@56.3210993,160.8371669,3a,70.1y,200.79h,91.44t/data=!3m7!1e1!3m5!1ssc6_n7JoUBi_j2amObSI1Q!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-1.4399999999999977%26panoid%3Dsc6_n7JoUBi_j2amObSI1Q%26yaw%3D200.79!7i13312!8i6656?entry=ttu&g_ep=EgoyMDI1MTAxNC4wIKXMDSoASAFQAw%3D%3D

このURLをわかりやすく区切ってみる。

https://www.google.com/maps/
@56.3210993,160.8371669,3a,70.1y,200.79h,91.44t
/data=
    !3m7
    !1e1
    ……(省略)

data= 以降のパラメーターも表示に関わっているが今回は無視して問題ない。

@56.3210993,160.8371669,3a,70.1y,200.79h,91.44t の部分が位置情報を示している。

, で区切った要素について、Embed APIとの対応関係をまとめる

URLの値 対応するEmbed APIのパラメーター
@56.3210993,160.8371669 緯度, 経度 location
70.1y 視野、ズームレベル。値が小さいほど画面をズームする。 fov
79h 方位、向いている方角。 heading
91.44t カメラの上下の向き。 pitch

あとはこの数値部分をパラメーターとしてAPIに渡せばOK。

……なのだが、pitch だけブラウザ上と APIで仕様が異なる。

ドキュメントによると、API のパラメーターでは pitch の有効な値は

-90°~ 90° の範囲の角度の値

だが、 91.44t は 90以上である。 そのまま渡すと青い空しか表示されない。

  • Embed APIでは -90が真下(地面)〜90が真上(空)
  • URLだと 0が真下〜180が真上

の仕様になっている模様。 なので、Embed API に渡すには-90する。

91.44 - 90 = 1.44

参考:pitchの指定を間違えると青い空しか表示されない

埋め込みURL

ブックマークレットを実行すると、下記の埋め込みURLが作成される。

https://www.google.com/maps/embed/v1/streetview?key=API_KEY&location=56.3210993,160.8371669&heading=200.79&pitch=1.4399999999999977&fov=70.1

notion埋め込み結果

コピペしてnotion に埋め込んでみると、ブラウザ表示と同様、山が望める地点が表示される。

画像として埋め込めるStreet View Static API もついでに試してみた。

パラメーターをほぼ使いまわせるが、 こちらは size で出力サイズの指定が必須。 また、無料利用に上限がある。

Embed URLと異なり、元の位置へのリンクがない、画像ファイルである。

Static APIの出力

参考

公式情報

Google map の URL仕様 (非公式)

Gist

# kuromoji.js で辞書にエントリーを追加する

形態素解析JavaScript で実行できるのは kuromoji.js 一択です。

mecab のようにユーザー辞書を使おうとしたのですが、 どうやらそのようなオプションはなさそうだったので、 kuromoji.js のビルド機能を使って辞書を構築し直しました。

kuromoji.js の辞書の仕組み

npm install で追加される node_modules の kuomoji において、 dict というフォルダがあり、そのフォルダの中に辞書ファイルがあります。

node_modules/kuromoji
├── dict
│   ├── base.dat.gz
│   ├── cc.dat.gz
│   ├── check.dat.gz
│   ├── tid.dat.gz
│   ├── tid_map.dat.gz
│   ├── tid_pos.dat.gz
│   ├── unk.dat.gz
│   ├── unk_char.dat.gz
│   ├── unk_compat.dat.gz
│   ├── unk_invoke.dat.gz
│   ├── unk_map.dat.gz
│   └── unk_pos.dat.gz

この辞書ファイルの構築方法は gulpfile で定義されていて (npm run build-dict)、だいたい以下のようなことをします。

  • mecab-ipadic-seed のパッケージを使って mecab-ipadic のファイルを取得
    • ファイルは node_modules/mecab-ipadic-seed/lib/dict に保存
  • mecab-ipadic-seed のビルドを実行
  • binary に変換
  • dict/hoge.dat.gz 形式でファイルとして保存

辞書データの参照先はnode moduleの mecab-ipadic-seed 、出力先は dict で固定です。

kuromoji.js で独自の辞書を構築

node module の kuromoji だと mecab-ipadic-seed がインストールされません。 また、辞書の入力・出力が固定なので、 実際に kuromoji.js を使う環境とは別で辞書を構築し、 構築したファイルを持ってくる方が安全といえます。

環境設定

ソースコードを clone してきて、独自辞書を構築します。

git clone https://github.com/takuyaa/kuromoji.js.git

kuromoji.js は開発が止まっていて、ライブラリが色々古いので、念の為 node のバージョンは 10系で実行します。 試しに 22.17.1 で実行したら以下のエラーになりました。

$ npm run build-dict

> kuromoji@0.1.2 build-dict
> gulp build-dict

fs.js:44
} = primordials;
    ^

ReferenceError: primordials is not defined

node v10.24.1 でパッケージをビルドし、初期設定で npm run build-dict が実行できるか確認します

npm run build
npm run build-dic

dict/ にファイルが作成されたらOKです。

辞書の作成

ユーザー辞書ファイルを作成します。 mecab-ipadic の書き方に従えば良いです。

文脈IDとコストは mecab の機能で自動推定も可能です。 今回は mecab-ipadic-neologd の登録から品詞、文脈ID、コストをそのまま使い回しました。

下記の通り、ipadic に登録されていないだろう2025年っぽいものにしています。

大阪・関西万博,1288,1288,5208,名詞,固有名詞,一般,*,*,*,大阪・関西万博,オオサカカンサイバンパク,オオサカカンサイバンパク
ミャクミャク,1288,1288,7806,名詞,固有名詞,一般,*,*,*,ミャクミャク,ミャクミャク,ミャクミャク

作成したファイルを mecab-ipadic-seed の辞書フォルダにおき、辞書を再度構築します。

cp ./sample.csv ./node_modules/mecab-ipadic-seed/lib/dict
npm run build-dic

これで dict/ に新しく辞書ファイルが作成されました。 一度作成したデータは上書きされるので管理には注意が必要です。

実行確認

作成できた辞書 /path/to/kuromoji.js/dict を kuromoji.js の呼び出しの時に指定してみます。

今回は kuromoji.js の wrapper である kuromojin を使いました。 kuromojin では tokenize の option で辞書を指定できます。

下記のサンプルコードは node.js の API として実装しようとしたものの使い回しなので typescript でコードを書いてます。 node v22.17.1 です。

// tokenizer.ts
import { tokenize } from "kuromojin";

export async function getTokens(text: string){
    return tokenize(text, {dicPath: "/path/to/kuromoji.js/dict"});
}

// テスト実行
const result = await getTokens("今日、大阪・関西万博でミャクミャクに出会った。");
console.log(result);

実行結果に 大阪・関西万博ミャクミャク が既知の単語 (word_type: 'KNOWN') として含まれています。

$ npx node --loader ts-node/esm src/tokenizer.ts

[
  {
    word_id: 126280,
    word_type: 'KNOWN',
    word_position: 1,
    surface_form: '今日',
    pos: '名詞',
    pos_detail_1: '副詞可能',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '今日',
    reading: 'キョウ',
    pronunciation: 'キョー'
  },
  {
    word_id: 91470,
    word_type: 'KNOWN',
    word_position: 3,
    surface_form: '、',
    pos: '記号',
    pos_detail_1: '読点',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '、',
    reading: '、',
    pronunciation: '、'
  },
  {
    word_id: 82720,
    word_type: 'KNOWN',
    word_position: 4,
    surface_form: '大阪・関西万博',
    pos: '名詞',
    pos_detail_1: '固有名詞',
    pos_detail_2: '一般',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '大阪・関西万博',
    reading: 'オオサカカンサイバンパク',
    pronunciation: 'オオサカカンサイバンパク'
  },
  {
    word_id: 70250,
    word_type: 'KNOWN',
    word_position: 11,
    surface_form: 'で',
    pos: '助詞',
    pos_detail_1: '格助詞',
    pos_detail_2: '一般',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'で',
    reading: 'デ',
    pronunciation: 'デ'
  },
  {
    word_id: 155300,
    word_type: 'KNOWN',
    word_position: 12,
    surface_form: 'ミャクミャク',
    pos: '名詞',
    pos_detail_1: '固有名詞',
    pos_detail_2: '一般',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'ミャクミャク',
    reading: 'ミャクミャク',
    pronunciation: 'ミャクミャク'
  },
  {
    word_id: 70290,
    word_type: 'KNOWN',
    word_position: 18,
    surface_form: 'に',
    pos: '助詞',
    pos_detail_1: '格助詞',
    pos_detail_2: '一般',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'に',
    reading: 'ニ',
    pronunciation: 'ニ'
  },
  {
    word_id: 530550,
    word_type: 'KNOWN',
    word_position: 19,
    surface_form: '出会っ',
    pos: '動詞',
    pos_detail_1: '自立',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '五段・ワ行促音便',
    conjugated_form: '連用タ接続',
    basic_form: '出会う',
    reading: 'デアッ',
    pronunciation: 'デアッ'
  },
  {
    word_id: 25950,
    word_type: 'KNOWN',
    word_position: 22,
    surface_form: 'た',
    pos: '助動詞',
    pos_detail_1: '*',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '特殊・タ',
    conjugated_form: '基本形',
    basic_form: 'た',
    reading: 'タ',
    pronunciation: 'タ'
  },
  {
    word_id: 91500,
    word_type: 'KNOWN',
    word_position: 23,
    surface_form: '。',
    pos: '記号',
    pos_detail_1: '句点',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '。',
    reading: '。',
    pronunciation: '。'
  }
]

ちなみに辞書指定なしだと「大阪・関西万博」は 大阪/・/関西/万博 と細かく分割され、

ミャクミャクは未知語 (word_type: 'UNKNOWN') として扱います。

  {
    word_id: 230,
    word_type: 'UNKNOWN',
    word_position: 12,
    surface_form: 'ミャクミャク',
    pos: '名詞',
    pos_detail_1: '一般',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '*'
  }

mecab-ipadic も kuromoji.js も更新が止まっているので、 最新の辞書で形態素解析をしたい時、カスタマイズしたい時に不便だなと感じました……。

WebAssembly の sudachi をみつけたものの、 レポジトリは public archiveで更新はなさそうです。

JavaScriptNLP をしようとすると制約が多いですね。悩ましい。

参考

# slack でチャンネルをまとめてアーカイブする slack_archive_bot をつくった

slack channel のアーカイブを自動化するために Slack App (Slack Bot)を活用した機能を開発しました。

github.com

指定した期間、投稿がないチャンネルをまとめてアーカイブします。 実行にはPython のコマンドを叩きます。

まだ動作確認しきれてはいませんが、一通りの機能は実装しました。 (一番大事なまとめてアーカイブ実行はこれから動作確認します……なにぶんアーカイブされてしまうので……)

実装する上で詰まった部分とか気になった部分についてまとめておきます。

背景

社内の slack の整理整頓を度々しています。

今までは、チャンネル管理ツールの チャンネルリストをエクスポート の機能でチャンネル一覧をCSVで取得し、 最終更新の日付で絞り込みし、ひとつずつslackアプリの画面からアーカイブしていました。

これだとかなり時間がかかってしまうので、自動化したいなとは思っていました。

先行事例

自動化する取り組みはすでに複数の方々が実例を公開してます。

いずれの例も外部ツール (gasp, sls, webhook) の連携をしています。

そもそも、Slack Botアーカイブするためには そのチャンネルに参加している 必要があります。

完全に自動化するためには、新しいチャンネルが作られるたびに Slack Botが入る機能も構築する必要があり、 定期実行が必須になります。

自分の作業を代替するという意味では、定期実行は不要なので今回上記の先行事例はそのまま使えません。 CLIアーカイブするだけのシンプルな機能を一から実装することにしました。

実装

シンプル かつ Bot が必要以上にチャンネルに出入りしない実装にしました。

アーカイブの実行対象のチャンネルにのみ Slack Bot が参加するようにします。

チャンネル一覧取得

チャンネル一覧の取得は conversations.list でできますが、このデータには最終更新日時情報がないです。

チャンネルに参加した上で、conversations.history で最新の投稿を取得する必要があります。

ちなみに slack の時刻情報は基本的に timestamp です。 小数点以下の値はエンドポイントによってあったりなかったりします。

{
    "type": "message",
    "user": "U123ABC456",
    "text": "I find you punny and would like to smell your nose letter",
    "ts": "1512085950.000216"
}

チャンネル数が多いと conversations.list + conversations.history で ratelimit に引っかかる懸念があります。

そこで チャンネル管理ツールと同じようなデータを出力できる admin.analytics.getFile を利用することにしました。

type=public_channelでチャンネル一覧を一度のリクエストで取得できます。

注意点としては、直近の日付のデータは取得できないです(定期実行で収集しているため?) 当日を指定するとおそらく data_not_available (The date was before the API became available.) エラーになります。

なので、確実に取得できるように1週間前のデータを指定しています。

また、実行に必要な権限 admin.analytics:read は User Token でしか実行できません。

全部 Bot Token (Slack Bot) に権限を集約させたかったのですが、仕方ないので Token を2つとも使います。

前述の通り、 conversations.listconversations.history であればSlack Botだけで対応できます。

アーカイブの実行

アーカイブ候補となっているチャンネルに Slack Botが参加し、アーカイブを実行します。

conversations.archive を使っています。

adminだと admin.conversations.bulkArchive がありますが、これまた User Token しか実行できなそうです。

まとめ

今回はチャンネルの一覧取得にだけ User Token (admin権限) を活用して実装しました。

組織によってアーカイブする方針が異なったり、権限周りの制限があると思います。

そういう意味では自分が今回実装したコードがそのまま使うのは難しいかもしれません。 Forkしてカスタマイズしたり、実装の参考になれば幸いです。

# AWS SAM で Image を使って実行しようとしたらエラーになったメモ (Error: 'NoneType' object has no attribute 'get')

調べても解決策が出てこなかったためブログにまとめておく。

エラー時のバージョン

$ aws --version
aws-cli/2.24.0 Python/3.12.9 Darwin/23.5.0 source/arm64
$ sam --version
SAM CLI, version 1.137.1

結論としては 「sam cli を最新にアップデートして解消」ではあるが、 エラーメッセージからまったく察することができず、結構苦しんだ。

経緯

SAM CLI を使って lambda をデプロイすることを目指す。 AWS Serverless Application Model デベロッパーガイドの チュートリアルに従ってテンプレートのプロジェクトを作成。

sam init

コードは Githubaws/aws-sam-cli-app-templates のテンプレートから生成される。

今回はPython3.12を指定したので元のテンプレートは python3.12/hello/{{cookiecutter.project_name}}/

作成したテンプレートに対し、spacy モデルを requirements.txt (hello_world/requirements.txt) に追加してデプロイしたところ容量オーバーのメッセージが出てしまった。

This AWS::Lambda::Function resource is in a CREATE_FAILED state. 
Resource handler returned message: "Unzipped size must be smaller than 262144000 bytes (Service: Lambda, Status Code: 400, Request ID: XXXX) (SDK Attempt Count: 1)" 
(RequestToken: XXXX, HandlerErrorCode: InvalidRequest)

Unzipped size must be smaller than 262144000 bytes というメッセージ通り、 モデルファイルなど、大きめのパッケージを使うには Docker image を使ってデプロイする必要がある。

なので、 template.ymlPackageType: Image を指定することにした。

Image を指定するよう修正

修正した template.yml は以下。 Resources 以外の部分はテンプレートから修正していないので省略した。

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Image
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get 
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./hello_world
      DockerTag: python3.12-v1

app.pyDockerfile は、テンプレートをそのまま利用。

# hello_world/Dockerfile
FROM public.ecr.aws/lambda/python:3.12

COPY app.py requirements.txt ./

RUN python3.12 -m pip install -r requirements.txt -t .

# Command can be overwritten by providing a different command in the template directly.
CMD ["app.lambda_handler"]

エラー内容

修正後、 sam build してからローカルで実行するとエラーになった。

$ sam validate
/Path/To/sam-image-app/template.yaml is a valid SAM Template

$ sam local invoke
Invoking Container created from helloworldfunction:python3.12-v1
Local image was not found.
Removing rapid images for repo helloworldfunction
Building image..................

Error: 'NoneType' object has no attribute 'get'
Traceback:
  File "click/core.py", line 1078, in main
  File "click/core.py", line 1688, in invoke
  File "click/core.py", line 1688, in invoke
  File "click/core.py", line 1434, in invoke
  File "click/core.py", line 783, in invoke
  File "samcli/cli/cli_config_file.py", line 347, in wrapper
  File "click/decorators.py", line 92, in new_func
  File "click/core.py", line 783, in invoke
  File "samcli/lib/telemetry/metric.py", line 185, in wrapped
  File "samcli/lib/telemetry/metric.py", line 150, in wrapped
  File "samcli/lib/utils/version_checker.py", line 43, in wrapped
  File "samcli/cli/main.py", line 95, in wrapper
  File "samcli/commands/local/invoke/cli.py", line 126, in cli
  File "samcli/commands/local/invoke/cli.py", line 235, in do_cli
  File "samcli/commands/local/lib/local_lambda.py", line 169, in invoke
  File "samcli/lib/telemetry/metric.py", line 325, in wrapped_func
  File "samcli/local/lambdafn/runtime.py", line 229, in invoke
  File "samcli/local/lambdafn/runtime.py", line 100, in create
  File "samcli/local/docker/lambda_container.py", line 122, in __init__

An unexpected error was encountered while executing "sam local invoke".
Search for an existing issue:
https://github.com/aws/aws-sam-cli/issues?q=is%3Aissue+is%3Aopen+Bug%3A%20sam%20local%20invoke%20-%20AttributeError
Or create a bug report:
https://github.com/aws/aws-sam-cli/issues/new?template=Bug_report.md&title=Bug%3A%20sam%20local%20invoke%20-%20AttributeError

Error: 'NoneType' object has no attribute 'get' はコードを見た限り、引数で渡されているはずの image_config がないのが原因のようだった。 設定ファイルとの対応関係が不明なためこれだけではわからず。

aws-sam-cli/samcli/local/docker/lambda_container.py at develop · aws/aws-sam-cli · GitHub

            _command = (image_config.get("Command") if image_config else None) or config.get("Cmd")

エラー原因の確認

chatGPT に聞きつつ解決方法を探った。引用部分は chatGPT から。

template.yaml に ImageUri が正しく設定されていない、または PackageType: Image がない

build して生成される .aws-sam/build/template.yamlImageUri があることを確認。

# 一部抜粋
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
      ImageUri: helloworldfunction:python3.12-v1
    Metadata:

docker でも確認。 なぜか 55 years ago になっているがそれらしきものはあった。 一度この image を削除して再度 sam build してみたものの解消せず。

$ docker images
REPOSITORY                            TAG                 IMAGE ID       CREATED        SIZE
helloworldfunction                    python3.12-v1       f8524b7c5d62   55 years ago   1.83MB
helloworldfunction                    rapid-x86_64        9aa75461724a   55 years ago   9.66MB

SAM CLI のバージョンを確認・更新 このエラーは古い SAM CLI に特有の場合もあります。

あまり古いバージョンではないはずだがアップデートした。

また、aws-clibrew でインストールしていたのだが、 sam clibrewのメンテナンスをしていないようだったので直接インストールする方法に変更した。

更新したバージョンは以下。

$ aws --version
aws-cli/2.27.12 Python/3.13.3 Darwin/23.5.0 exe/x86_64
$ sam --version
SAM CLI, version 1.138.0

これで再度 build。

$ sam build
Building codeuri: /Path/to/sam-image-app runtime: None architecture: x86_64 functions: HelloWorldFunction
Building image for HelloWorldFunction function
Setting DockerBuildArgs for HelloWorldFunction function
Step 1/4 : FROM public.ecr.aws/lambda/python:3.12

# 中略

Successfully built e96027022969
Successfully tagged helloworldfunction:python3.12-v1


Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided

ローカル実行できた。

$ sam local invoke
Invoking Container created from helloworldfunction:python3.12-v1
Building image.................
Using local image: helloworldfunction:rapid-x86_64.

START RequestId: 68678ddf-991f-4d70-a6d6-21e079357d6e Version: $LATEST
END RequestId: 815c324c-50d7-4748-9c8d-6cf90c6f129d
REPORT RequestId: 815c324c-50d7-4748-9c8d-6cf90c6f129d  Init Duration: 0.76 ms  Duration: 405.98 ms Billed Duration: 406 ms Memory Size: 128 MB Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

image も直近で作成したことになっている。

$ docker images
REPOSITORY                            TAG                 IMAGE ID       CREATED          SIZE
helloworldfunction                    python3.12-v1       e96027022969   13 seconds ago   745MB
helloworldfunction                    rapid-arm64         2540e2a1b874   29 minutes ago   812MB

AWS のツールは更新頻度が高いので、定期的に更新するよう心がけないといけないなと思った。

ちなみに Zip だとデプロイはできるが、sam local invoke で同じエラーが出る。 SAM CLI と docker の依存関係の問題なのだろうか。

参考

# NLP2025読んだ・聴いた論文メモ

昨年 と同様、言語処理学会年次大会の論文についてまとめます。

予稿集ページの発表一覧 の掲載してある順に紹介していきます。

P1-19 ニューラルかな漢字変換システム Zenzai

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/P1-19.pdf

P1-20 低資源言語のニュース機械翻訳のためのLLM を用いた合成対訳データの生成

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/P1-20.pdf

  • 低資源言語 (タイ語) から日本語への翻訳
  • LLMでデータを増やす (特定のジャンル・トピックを指定して生成)
    • ドメイン・ジャンル・タイ特有の表現に対応できる

Q01-03 JETHICS: 日本語道徳理解度評価用データセット

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/Q1-3.pdf

Q1-24J MATCHA:専門家が平易化した記事を用いたやさしい日本語パラレルコーパス

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/Q1-24.pdf

  • 既存研究と比較して、高品質・大規模なテキスト平易化のコーパス
  • データは訪日観光客向けのサイトMATCHAを使用し、文のアライメントをとっている
  • Github: EhimeNLP/matcha

P2-11 誤字に対するTransformerベースLLMのニューロンおよびヘッドの役割調査

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/P2-11.pdf

  • 誤字を含む入力がモデルに与える影響について調査
    • 誤字を含んでいてもモデルは正しい推論をすることが多い
  • どのニューロン、アテンションヘッドが誤字の入力を認識・修復しているか

P3-1 RoBERTaとT5を用いた2段階モデルによる国語答案の文字認識誤り訂正

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/P3-1.pdf

  • 手書きの答案の自動採点に向けた取り組み
  • 手書き文字のOCRには誤りが含まれるので、修正したい
  • 誤り箇所推定の後段で誤り訂正することで必要のない箇所の訂正を防ぐ

Q3-1 移動軌跡に関する質問応答データセット

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/Q3-1.pdf

  • 人の移動データ(位置情報の系列)を解釈するためのデータセット
  • 移動軌跡と質問を入力としてQAを行う
    • 問題は事実照会問題・選択式問題・自由記述式問題
  • 移動軌跡のデータは jeffmur/geoLife を使用

Q5-21 テキスト埋め込みからのテキスト復元における予測制御の援用の効果検証

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/Q5-21.pdf

  • テキスト埋め込みから、元の入力テキストを復元する
  • 予測制御 (将来の出力を予測しながら現在の行動を決定する制御手法) でテキストを生成

P8-20 SoftMatcha: 大規模コーパス検索のための柔らかくも高速なパターンマッチャー

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/P8-20.pdf

  • 単語埋め込みを利用したテキスト検索
  • コサイン類似度で 柔らかくマッチ するため、単語の表記揺れ・類義語も含めて検索できる
  • 転置索引を構築することで高速に動作する
  • Github: softmatcha/softmatcha

B10-5 読み推定のための教師なし単語分割

https://www.anlp.jp/proceedings/annual_meeting/2025/pdf_dir/B10-5.pdf

終わりに

修学旅行以来の長崎訪問でした。

会場の出島メッセを含め、 長崎駅周辺の再開発が進んでいる雰囲気がありました。 新幹線が開通していたのも驚きです。

今回は観光する余裕がなかったのですが、 個人的に大好きな菓子パン「マンハッタン」が食べられて満足です。

九州でしか売っていないのでね……。

固めドーナツにチョコがコーティングされているカロリーの塊、マンハッタン

eieito.hatenablog.com

# 大字・町丁目・街区の位置参照情報を geopandas で可視化

国土数値情報ダウンロードサイト位置参照情報を使って住所を調べるのって本当に可能なのか気になったのでデータを見てみた。

都道府県のデータはあるが、今回は東京都の位置参照情報を使った。 コードは gist でアップロードしている。

gist geopandas_位置参照情報.ipynb · GitHub

位置参照情報には大字・町丁目レベルと街区レベルがあるのでそれぞれデータを解説する。

tl;dr

  • 大字・町丁目は市区町村の後に続く地名 (+N丁目)
    • 代表点であり、必ずしもその地名の内側にあるとは限らない
  • 街区は大字・町丁目の後に続くN番の部分 (大体数値)
    • 一部地域は未整備
  • 大字・町丁目レベルと街区レベル、どちらも点のデータ

大字・町丁目レベル

データには都道府県、市区町村、大字町丁目、緯度・経度が含まれる。

大字(おおあざ)は市区町村の後に続く地名のこと。

大字・町丁目データは大字の時もあれば町丁目の場合もある。 データがどちらのカテゴリかは "大字・字・丁目区分コード" で区別できる。 (東京都だと町丁目を含む場合が多いが他地域は未検証)

例えば国会議事堂の住所、東京都千代田区永田町一丁目7番1号 なら 永田町一丁目 が大字・丁目名。

町丁目を含まない大字・丁目名は 東京都中央区浜離宮庭園浜離宮庭園東京都千代田区紀尾井町紀尾井町 などがある。 ちなみに○丁目の○は必ず漢数字で入っている。

geopandas で大字・町丁目レベルのデータを読み込んでプロットすると以下のようになる。 島を含むため広い範囲が表示されている。

大字・町丁目レベルのデータ

23区だけに絞り込むと均等に色がプロットされる。飛地はなさそうだ。

23区データ
後述するが、大字・町丁目レベルのデータは大字・町丁目の代表点なので、正確な住所を取得したい場合には適していない。

街区レベル

大字・町丁目よりさらに細かい、○番まで含まれるデータが街区レベルのデータ。 "街区符号・地番" データが含まれる。

例えば、 永田町一丁目 の1番, 2番, 3番,... それぞれの位置情報がわかる。 (ただし、"街区符号・地番" は数値とは限らない)

(また、今回は触っていないが平面直角座標系も含まれている。)

東京都中央区で、大字・町丁目と街区をプロットすると街区の方が細かくプロットされるのがわかる。

東京都中央区の街区 (+大字・町丁目). 大字・丁目名がダイヤ(◆)

contextily を使って OpenStreetMap の地図と重ねてみると、1つのブロックに1つの街区が重なっている。

OpenStreetMapを重ねた結果

1つの大字・丁目に注目して確かめると、中央区は比較的ブロックごとに街区が決まっていそうにみえる。

中央区日本橋浜町二丁目

区画整備がされているかどうかは地域差があるので注意。 場所によっては、地図と重ねてもどこが街区の切れ目かわからない。

千代田区紀尾井町. 街区の丸マーク (●) が点在

大字・町丁目レベルと街区レベルデータの扱い

対応関係

大字・町丁目のデータと、街区のデータは必ずしも対応関係があるわけでない。 東京都の場合、以下の市区町村は街区のデータがなかった。

'利島村', '御蔵島村', '西多摩郡奥多摩町', '西多摩郡檜原村', '青ヶ島村'

サイトにも下記の通り記載されており、街区は未整備な地域が多いようだ。

「街区レベル位置参照情報」は、GISや空間データの利用・普及のための基本的な情報として整備していますが、データの整備範囲は都市計画区域内に限られています。

「大字・町丁目レベル位置参照情報」は「街区レベル位置参照情報」未整備地域を含む全国で整備を実施しており、「街区レベル位置参照情報」を補完する位置参照情報です。

https://nlftp.mlit.go.jp/isj/index.html

大字・町丁目の位置

また、大字・町丁目レベルの位置は代表点ではあるものの、 対応する街区の内側にあるとは限らない。

これは島部の例がわかりやすい。 伊豆諸島に絞ってプロットしてみると、 大字・町丁目が何もない部分に位置している「大字・丁目名」が何個かある。 街区が離れている場所が存在しているのが原因かもしれない。

八丈町 (八丈島). 大字・丁目名である青のダイヤ(◆)がポツンとあることがわかる

八丈町大賀郷. 大字・丁目名である橙のダイヤ(◆)が中央部分にある

なので、位置や距離を扱いたい場合はより細かい街区レベルのデータを使うのが適していそうに見える。

どちらにせよ、これらの位置データは住所の代表 。 区画はわからないので正確な位置と住所の対応関係が知りたい場合や、大字・町丁目ごと塗りつぶしたい場合には使えない。

ポリゴンデータは調べた限り有料の提供しかなさそう。

参考資料