AI手帖

文字TF-IDFをPythonで計算する — 0の値と「語彙にない」を見分ける

「ログインできません」という架空の問い合わせ文を文字n-gramで数値化すると、3文字の「ログイ」には約0.2413の重みが付き、語彙に存在し文中に現れない「料金」の重みは0となります。一方、4文字の「ログイン」は特徴量として語彙に存在しません。学習用45件から構築した1,056種類の語彙をもとに、TF-IDFの計算式、未正規化L2ノルム15.456621...による正規化、未知文字への挙動を実測値から確認します。

この記事の流れ

文字n-gramによる特徴抽出と語彙の構築

機械学習モデルに日本語テキストを入力するため、文字列を数値ベクトルへ変換する手法として、文字単位のn-gramを用いた TfidfVectorizer を検証します。単語辞書に依存する形態素解析は行いません。

この実習では、scikit-learnの TfidfVectorizer(analyzer='char', ngram_range=(1, 3)) を指定します。この設定では、単語の区切りを考慮せず、テキスト中に出現する連続した1文字、2文字、3文字の部分文字列をすべて切り出して特徴量として扱います。

たとえば、「ログイン」という文字列は4文字で構成されているため、今回指定した1〜3文字の範囲では「ログイン」という単語全体が1つの特徴量になることはありません。その代わり、1文字の「ロ」「グ」「イ」「ン」、2文字の「ログ」「グイ」「イン」、3文字の「ログイ」「グイン」という断片に分解され、それぞれが独立した特徴量となります。

長さ「ログイン」から作る断片
1文字ロ、グ、イ、ン
2文字ログ、グイ、イン
3文字ログイ、グイン

上の表は、入力文字列から文字n-gramが切り出される様子を示しています。単語辞書を使わないため、未知の複合語や表記のばらつきに対しても部分文字列の組み合わせとして捉えることができます。

TfidfVectorizer は、入力されたテキスト群を解析して文字n-gramの辞書(語彙)を構築します。この語彙作成処理が fit です。

この実習では、学習用として用意された45件の架空問い合わせ文のみを用いて fit を実行しています。この45件の学習文から1〜3文字の並びを網羅した結果、登録された語彙数は 1,056 種類となりました。したがって、この変換器を用いて文章をベクトル化(transform)すると、どのような文章を入力しても1,056成分のベクトルが得られます。

語彙の作成およびIDFの重み計算には学習用の45件のみを使用し、調整用データや固定比較用データを fit に含めていません。比較用の文にある文字の並びや文書頻度を、変換器へ事前に取り込まないためです。scikit-learn公式の データ漏洩に関する注意 でもこの手順が示されています。

TF-IDF重み付けの計算式とL2正規化

語彙を構築した後、入力された文章に対して各特徴量の重みを計算します。ここでは、入力文「ログインできません」を変換した際の具体的な数値を追います。

断片語彙への登録入力内の回数学習45文で出る文数IDFL2正規化後の値
ログイあり123.7300290.241322
グインあり123.7300290.241322
ログあり123.7300290.241322
料金あり023.7300290.000000
ログインなし———成分なし

上の表は、「ログインできません」を変換した際の特徴値を示しています。TF-IDFは、対象文内における語句の出現頻度(TF: Term Frequency)と、コーパス全体における珍しさを表す逆文書頻度(IDF: Inverse Document Frequency)の積として計算されます。

今回使用したscikit-learnの設定は、平滑化を適用する smooth_idf=True かつ sublinear_tf=False です。学習文の総数を N=45、学習文の中で対象の文字n-gramが含まれていた文書数を df とすると、IDFは自然対数を用いて以下の式で計算されます。

log((1 + N) / (1 + df)) + 1

計算式の詳細は scikit-learn公式のTF-IDF重み付け解説 に記載されています。

具体例として、3文字の特徴量「ログイ」の数値を計算します。入力文「ログインできません」の中に「ログイ」は1回出現するため、TFは1です。学習文45件を調べると「ログイ」が含まれる文書は2件存在するため、df = 2 です。公式に代入すると、log((1 + 45) / (1 + 2)) + 1、すなわち log(46 / 3) + 1 となり、IDF値は約 3.730029... となります。入力文中に1回出現するため、未正規化の重みも 1 * 3.730029... = 3.730029... です。

文章の長さによる影響を揃えるため、scikit-learnの TfidfVectorizer はデフォルトで各ベクトルのL2ノルム(二乗和の平方根)が1になるよう正規化(norm='l2')を行います。入力文「ログインできません」から抽出された非ゼロの特徴量は合計24個あります。これら全24個の成分の二乗和を求めて平方根を計算すると、ベクトル全体の未正規化L2ノルムは 15.456621... となります。「ログイ」の未正規化値 3.730029... をこの全24成分の未正規化L2ノルム 15.456621... で割ることで、最終的な特徴値である 0.241322...(約0.2413)が得られます。

非ゼロベクトルをL2正規化すると、そのベクトルの長さ(L2ノルム)は数値の丸め誤差の範囲で1となります。一方、成分の数値を単純に足し合わせた合計は約 4.784694 です。したがって、0.2413 という値は分類確率や全体の割合を示すものではありません。

上の表における「料金」と「ログイン」の扱いの違いも確認します。「料金」は学習45件に出てくるため語彙(1,056種類)の中に存在しますが、今回の入力文には現れないため重みは 0.0 となります。一方、4文字の「ログイン」は ngram_range=(1, 3) の指定により語彙に登録されていません。ベクトルの中にNoneが入っているわけではなく、掲載コードの辞書参照 vectorizer.vocabulary_.get(term) が未登録の項目にNoneを返します。語彙に存在して値が0であることと、語彙辞書に登録されていないことは異なります。

未知文字への挙動と文字n-gram範囲の拡張

学習データに含まれていない文字が入力された場合の挙動を確認するため、絵文字のみで構成された検証文字列「🦊🪐」を入力します。

この文字列から切り出されるn-gramは「🦊」「🪐」「🦊🪐」ですが、これらはいずれも学習用45件から作成した語彙(1,056種類)には存在しません。このとき得られるベクトルは、非ゼロ成分の数が 0、ベクトルのL2ノルムも 0.0 となります。これは入力テキストが空であることを意味するのではなく、入力文字列の中に学習済み語彙で表現できる文字断片が1つも含まれていなかった状態を示しています。

なお、文章の中に未知の単語が含まれていたとしても、既知の文字断片が1つでも含まれていれば、その成分に値が入るためベクトル全体がゼロになるわけではありません。

次に、抽出する文字n-gramの範囲を指定する ngram_range を (1, 4) に変更し、同じ45件の学習文を使って fit を実行します。

文字数の範囲語彙の種類数「ログイン」を丸ごと登録
1〜3文字1,056なし
1〜4文字1,514あり

上の表が示す通り、対象とする文字長の上限を3文字から4文字に広げると、登録される語彙数は 1,056種類 から 1,514 種類へと458種類増加しました。この変更により、先ほどは語彙に存在しなかった4文字の「ログイン」という文字列も、独立した特徴量として語彙に追加されます。

本実験では語彙の構成とベクトルの形状を実測したものであり、分類器の学習や精度の比較は実施していません。また、語彙の構成やIDFの重みは fit に使用したデータセットの統計情報に依存するため、別のデータで fit を行った場合は重みの値が変わり得ます。

値を再計算して、文字の範囲を変える

語彙を作る文と、変換する文を分けるコードの抜粋です。

import json
from pathlib import Path
from sklearn.feature_extraction.text import TfidfVectorizer

data = json.loads(Path("dataset.json").read_text())
train_text = [text for text, label in data["train"]]
vectorizer = TfidfVectorizer(analyzer="char", ngram_range=(1, 3))
vectorizer.fit(train_text)
row = vectorizer.transform(["ログインできません"])

for term in ["ログイ", "料金", "ログイン"]:
    column = vectorizer.vocabulary_.get(term)
    print(term, "語彙にない" if column is None else float(row[0, column]))

実習ファイルを展開し、text-labsフォルダで次を実行します。

python -m pip install -r requirements.txt
python run-tfidf.py --out tfidf-results-local.json

まずはngram_rangeだけを(1, 1)や(1, 2)に変え、語彙数と特徴名を確認できます。分類器へつなぐ手順はXGBoostの実習で扱っています。

検証範囲と再現ファイル

2026年10月22日JST、AIアシスタントがLinuxサーバーCPUで実行しました。Python 3.11.2、NumPy 2.4.6、SciPy 1.16.2、scikit-learn 1.7.2です。架空教材の学習45文だけで語彙とIDFをfitし、調整9文と固定比較18文はfitに含めていません。Colab、GPU、実顧客データでの測定や、分類精度の比較は含みません。

全成分・IDF・語彙の記録 · 実行コード · ライブラリ指定 · 実行条件

初回のパッケージ取得には通信が必要です。再実行では時刻や環境が変わるため、JSON全体ではなく特徴名と数値を照合します。浮動小数点値には環境差が出る場合があります。

参照資料と実行記録

確認日:2026年10月22日。