http://scikit-learn.org/0.18/modules/feature_extraction.html をgoogle翻訳した
scikit-learn 0.18 ユーザーガイド 4. データセット変換 より
4.2 特徴抽出
sklearn.feature_extraction
モジュールは、テキストや画像などのフォーマットからなるデータセットから機械学習アルゴリズムでサポートされている形式の特徴量を抽出するために使用できます。
4.2.1. dictsからの特徴量のロード
クラスDictVectorizerは、標準のPython dictオブジェクトのリストとして表される特徴量配列を、scikit-learn推定器で使用されるNumPy / SciPy表現に変換するために使用できます。
特に処理が速いわけではありませんが、Pythonのdictには、使いやすく、疎である(存在しない特徴を格納する必要はありません)、値に加えて特徴名を格納できるという利点があります。
DictVectorizerは、カテゴリ(または、名目、離散値)に対して、1対1のコーディングまたは「ワンホット」コーディングを実装ます。カテゴリ属性は、値に順番のない離散性のリスト(トピック識別子、オブジェクトのタイプ、タグ、名前など)に制限される「属性 - 値」のペアです。
以下では、「city」はカテゴリ属性であり、「temperature」は従来の数値特徴量である。
>>> measurements = [
... {'city': 'Dubai', 'temperature': 33.},
... {'city': 'London', 'temperature': 12.},
... {'city': 'San Fransisco', 'temperature': 18.},
... ]
>>> from sklearn.feature_extraction import DictVectorizer
>>> vec = DictVectorizer()
>>> vec.fit_transform(measurements).toarray()
array([[ 1., 0., 0., 33.],
[ 0., 1., 0., 12.],
[ 0., 0., 1., 18.]])
>>> vec.get_feature_names()
['city=Dubai', 'city=London', 'city=San Fransisco', 'temperature']
DictVectorizerは、自然言語処理モデルの文章分類器の訓練に便利な表現変換です(典型的には、特定の関心のある単語の周辺から特徴量を抽出する)。
例えば、文章から品詞(PoS)タグを抽出する第1のアルゴリズムを有すると仮定する。 次の dict は、「The cat sat on the mat.」という文の中の「sat」という単語の周辺から抽出されたそのような特徴です。
>>> pos_window = [
... {
... 'word-2': 'the',
... 'pos-2': 'DT',
... 'word-1': 'cat',
... 'pos-1': 'NN',
... 'word+1': 'on',
... 'pos+1': 'PP',
... },
... # 実際のアプリケーションでは、多くのこのようなdictを抽出するでしょう
... ]
このデータは、分類器に供給するのに適した疎な2次元マトリックスにベクトル化することができます(正規化のためにtext.TfidfTransformerにパイプされた後)。
>>> vec = DictVectorizer()
>>> pos_vectorized = vec.fit_transform(pos_window)
>>> pos_vectorized
<1x6 sparse matrix of type '<... 'numpy.float64'>'
with 6 stored elements in Compressed Sparse ... format>
>>> pos_vectorized.toarray()
array([[ 1., 1., 1., 1., 1., 1.]])
>>> vec.get_feature_names()
['pos+1=PP', 'pos-1=NN', 'pos-2=DT', 'word+1=on', 'word-1=cat', 'word-2=the']
あなたが想像することができるように、文書コーパスの個々の単語の周りにこのような文脈を抽出すると、結果として得られる行列は非常に幅広くなり(多くのone-hot-features)、ほとんどがゼロになります。結果のデータ構造をメモリに収めるために、DictVectorizer
クラスはnumpy.ndarray
の代わりにデフォルトでscipy.sparse
行列を使用します。
4.2.2. 特徴ハッシュ
FeatureHasherクラスは、フィーチャハッシュまたは「ハッシングトリック」として知られている技術を使用する高速、低メモリベクタライザです。特徴値の変換テーブルを構築する代わりに、FeatureHasherは特徴値にハッシュ関数を適用して、サンプル行列の列番号を直接決定します。その結果、検査可能性を犠牲にしてスピードが上がり、メモリ使用量が減少します。ハッシャーは、入力特徴値の外観を覚えておらず、inverse_transform
メソッドもありません。
ハッシュ関数は(無関係な)特徴間で衝突を引き起こす可能性があるため、符号付きハッシュ関数が使用され、ハッシュ値の符号は特徴値の出力マトリックスに格納された値の符号を決定します。このようにして、衝突は誤差を累積するのではなく相殺される可能性が高く、出力特性の値の期待平均はゼロです。
non_negative = True
がコンストラクタに渡された場合、絶対値が取得されます。これは、衝突処理のいくつかを元に戻しますが、非負入力を期待するsklearn.naive_bayes.MultinomialNB 推定器や sklearn.feature_selection.chi2 特徴選択器などに出力を渡すことができます。
FeatureHasherは、コンストラクタパラメータinput_type
に応じて、マッピング(Pythonのdictやcollections
モジュールにあるその変種など)、(feature, value)
のペア、または文字列のリストを受け入れます。マッピングは(feature, value)
のリストとして取り扱われ、 ['feat1'、 'feat2'、 'feat3']
は[('feat1', 1), ('feat2', 1), ('feat3', 1)]
と解釈されます。 1つの特徴値がサンプル内で複数回出現する場合、関連する値は合計されます(したがって ('feat',2)
と ('feat',3.5)
は( 'feat',5.5)
になります。 FeatureHasherからの出力は、常にCSR形式のscipy.sparse
マトリックスです。
フィーチャハッシュはドキュメント分類に使用できますが、text.CountVectorizerと異なり、FeatureHasherはUnicodeからUTF-8エンコーディング以外の単語分割やその他の前処理は行いません。トークン化/ハッシャーを結合したものについては、下記のハッシュトリックを使用した大きなテキストコーパスのベクトル化を参照してください。
例として、(token、part_of_speech)
対から抽出された特徴を必要とする単語レベルの自然言語処理タスクを考える。特徴量を抽出するためにPythonジェネレータ関数を使うことができます:
def token_features(token, part_of_speech):
if token.isdigit():
yield "numeric"
else:
yield "token={}".format(token.lower())
yield "token,pos={},{}".format(token, part_of_speech)
if token[0].isupper():
yield "uppercase_initial"
if token.isupper():
yield "all_uppercase"
yield "pos={}".format(part_of_speech)
次に、FeatureHasher.transform
に供給されるraw_X
は、以下を使用して構築できます。
raw_X = (token_features(tok, pos_tagger(tok)) for tok in corpus)
次のようにしてハッシャーに供給されます。
hasher = FeatureHasher(input_type='string')
X = hasher.transform(raw_X)
scipy.sparse
行列X
を得る。
特徴の抽出を楽にするジェネレーターに注意してください。トークンは、ハッシャーからの要求に応じてのみ処理されます。
4.2.2.1. 実装の詳細
FeatureHasherはMurmurHash3の符号付き32ビット版を使用します。結果として(そしてscipy.sparseの制限のために)、現在サポートされているフィーチャの最大数は $2^{31}-1$ です。
ワインバーガー(Weinberger)らによるオリジナルのハッシングトリックの定式化では、 2つの別々のハッシュ関数 $h$ と $\xi$ を使用して、それぞれ特徴値の列番号と符号を決定しました。現在の実装は、MurmurHash3の符号ビットが他のビットから独立しているという前提のもとで動作します。
単純に割り算の余りを使用してハッシュ関数を列番号に変換するので、n_features
パラメータとして2の累乗を使用することをお勧めします。そうしないと、特徴値は均等にマップされません。
- 参考文献:
- Kilian Weinberger、Anirban Dasgupta、John Langford、Alex Smola、Josh Attenberg(2009)大規模なマルチタスク学習のための機能ハッシング Proc. ICML.
- MurmurHash3
4.2.3. テキスト特徴抽出
4.2.3.1. Bag of Words による表現
テキスト解析は、機械学習アルゴリズムの主要な応用分野です。しかし、生データでは、一連の記号をアルゴリズム自体に直接供給することはできません。なぜならそれらの大部分は可変長の生テキスト文書ではなく固定サイズの数値特徴ベクトルを期待するからです。
これに対処するために、scikit-learnはテキストコンテンツから数値特徴量を抽出するための最も一般的な方法のユーティリティを提供します。
- 文字列をトークン化し、トークンセパレータとして空白と句読点を使用するなど、可能なトークンごとに整数のIDを与えます。
- 各文書のトークンの出現を数えます。
- 大多数のサンプル/ドキュメントで発生する重要度の低いトークンを使用して正規化し、重み付けします。
このスキームでは、特徴およびサンプルは以下のように定義される。
- 個々のトークンの出現頻度が特徴量として扱われる(正規化されている時もそうでない時もある)。
- 与えられた文書のすべてのトークン頻度のベクトルは、多変量サンプルとみなされます。
従って、文書のコーパスは、文書当たり1つの行と、コーパスに現れるトークン(例えば、単語)ごとに1つの列とを有する行列によって表すことができる。
ベクトル化は、テキスト文書の集合を数値の特徴ベクトルに変換する一般的なプロセスです。この特定の戦略(トークン化、計数、正規化)は、Bag of Wordsまたは「Bag of n-grams」表現と呼ばれます。文書は、文書内の単語の相対位置情報を完全に無視して単語の出現数によって記述される。
4.2.3.2. 疎
ほとんどのドキュメントは一般にコーパスで使用されている単語のごく一部を使用するため、結果の行列には多くのゼロ(通常は99%以上)の特徴値が含まれます。
例えば、10,000の短文文書(電子メールなど)の集合は、合計10万オーダーの語彙を使用し、各文書は100から1000のユニークワードを使用する。
このような行列をメモリに格納できるだけでなく、代数演算行列/ベクトルを高速化するために、実装では通常、scipy.sparse
パッケージの疎な表現を使用します。
4.2.3.3. 共通ベクタライザの使用法
CountVectorizer は、トークン化と出現回数の計数の両方を1つのクラスに実装しています。
>>> from sklearn.feature_extraction.text import CountVectorizer
このモデルには多くのパラメータがありますが、デフォルト値は非常に妥当です(詳細はリファレンスドキュメント を参照してください):
>>> vectorizer = CountVectorizer(min_df=1)
>>> vectorizer
CountVectorizer(analyzer=...'word', binary=False, decode_error=...'strict',
dtype=<... 'numpy.int64'>, encoding=...'utf-8', input=...'content',
lowercase=True, max_df=1.0, max_features=None, min_df=1,
ngram_range=(1, 1), preprocessor=None, stop_words=None,
strip_accents=None, token_pattern=...'(?u)\\b\\w\\w+\\b',
tokenizer=None, vocabulary=None)
それを使って、テキスト文書の最小限のコーパスの単語の出現をトークン化して数えましょう。
>>> corpus = [
... 'This is the first document.',
... 'This is the second second document.',
... 'And the third one.',
... 'Is this the first document?',
... ]
>>> X = vectorizer.fit_transform(corpus)
>>> X
<4x9 sparse matrix of type '<... 'numpy.int64'>'
with 19 stored elements in Compressed Sparse ... format>
デフォルト設定では、2文字以上の単語を抽出して文字列をトークン化します。このステップを実行する特定の機能は、明示的に要求できます。
>>> analyze = vectorizer.build_analyzer()
>>> analyze("This is a text document to analyze.") == (
... ['this', 'is', 'text', 'document', 'to', 'analyze'])
True
フィッティング中にアナライザによって検出された各用語には、結果の行列の列に対応する固有の整数インデックスが割り当てられます。この列の解釈は、次のように取得できます。
>>> vectorizer.get_feature_names() == (
... ['and', 'document', 'first', 'is', 'one',
... 'second', 'the', 'third', 'this'])
True
>>> X.toarray()
array([[0, 1, 1, 1, 0, 0, 1, 0, 1],
[0, 1, 0, 1, 0, 2, 1, 0, 1],
[1, 0, 0, 0, 1, 0, 1, 1, 0],
[0, 1, 1, 1, 0, 0, 1, 0, 1]]...)
特徴値から列インデックスへの逆のマッピングは、ベクタライザのvocabulary_属性に格納されます。
>>> vectorizer.vocabulary_.get('document')
1
したがって、訓練コーパスに見られなかった単語は、変換メソッドへの今後の呼び出しで完全に無視されます。
>>> vectorizer.transform(['Something completely new.']).toarray()
...
array([[0, 0, 0, 0, 0, 0, 0, 0, 0]]...)
前のコーパスでは、最初の文書と最後の文書はまったく同じ単語を持ち、等価ベクトルでコード化されていることに注意してください。 特に、最後の文書が疑問の書式であるという情報を失う。順番の情報を保存するために、1グラム(個々の単語)に加えて2グラムの単語を抽出することができます。
>>> bigram_vectorizer = CountVectorizer(ngram_range=(1, 2),
... token_pattern=r'\b\w+\b', min_df=1)
>>> analyze = bigram_vectorizer.build_analyzer()
>>> analyze('Bi-grams are cool!') == (
... ['bi', 'grams', 'are', 'cool', 'bi grams', 'grams are', 'are cool'])
True
このベクトル化器によって抽出された語彙は、はるかに大きくなり、符号化された際の語順位置情報のあいまいさを解決することができます。
>>>
>>> X_2 = bigram_vectorizer.fit_transform(corpus).toarray()
>>> X_2
...
array([[0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0],
[0, 0, 1, 0, 0, 1, 1, 0, 0, 2, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1]]...)
具体的には、「Is this」という質問刑は最後の文書にのみ存在します。
>>> feature_index = bigram_vectorizer.vocabulary_.get('is this')
>>> X_2[:, feature_index]
array([0, 0, 0, 1]...)
4.2.3.4. Tf-idf重み付け
大きなテキストコーパスでは、いくつかの単語が非常に多く存在し(例えば、英語では「the」、「a」、「is」)、文書の内容に関する意味のある情報をほとんど持たない。頻度情報を直接分類器に直接供給すると、非常に頻繁に使用される用語は、希少ではあるが興味深い用語の頻度を隠します。
頻度情報を分類器に適した値にするため、tf-idf変換を使用することが非常に一般的である。
Tf-idfは単語出現数(tf)と出現文章数の逆数(idf)を意味し、$\text{tf-idf(t,d)}=\text{tf(t,d)} \times \text{idf(t)}$ です。
TfidfTransformer
のデフォルト設定 TfidfTransformer(norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False)
を使うと、
単語頻度、単語が所与の文書内で発生する回数は次のように計算されたIDF成分と乗算されます。
\text{idf}(t) = log{\frac{1 + n_d}{1+\text{df}(d,t)}} + 1
ここで $n_d$ は文書の総数であり、$\text{df}(d,t)$ は単語 $t$ を含む文書の数です。得られたtf-idfベクトルは、ユークリッドノルムによって正規化される。
v_{norm} = \frac{v}{||v||_2} = \frac{v}{\sqrt{v{\_1}^2 + v{\_2}^2 + \dots + v{\_n}^2}}
これはもともと、文書の分類やクラスタリングでよく使われている情報検索用に開発された単語重み付け方式(検索エンジンの検索結果の順位付け機能)でした。
以下のセクションでは、tf-idfsがどのように正確に計算され、scikit-learnの TfidfTransformerと TfidfVectorizer で計算されたtf-idfsが、idfを次のように定義する標準の教科書の表記法と少し違っているかを示しています。
\text{idf}(t) = log{\frac{n_d}{1+\text{df}(d,t)}}
TfidfTransformerおよびTfidfVectorizerでは、smooth_idf = False
にするとidfの分母の代わりにidfに "1"が追加されます。
$\text{idf}(t) = log{\frac{n_d}{\text{df}(d,t)}} + 1$
この正規化は、TfidfTransformerクラスによって実装されます。
>>> from sklearn.feature_extraction.text import TfidfTransformer
>>> transformer = TfidfTransformer(smooth_idf=False)
>>> transformer
TfidfTransformer(norm=...'l2', smooth_idf=False, sublinear_tf=False,
use_idf=True)
再度、すべてのパラメータの詳細については、リファレンスドキュメント を参照してください。
以下のような例を考えてみましょう。最初の単語の出現率は100%であり、それほど興味深いものではありません。 他の2つの特徴の出現率は50%未満しかないため、おそらくドキュメントの内容をよりよく表します。
>>> counts = [[3, 0, 1],
... [2, 0, 0],
... [3, 0, 0],
... [4, 0, 0],
... [3, 2, 0],
... [3, 0, 2]]
...
>>> tfidf = transformer.fit_transform(counts)
>>> tfidf
<6x3 sparse matrix of type '<... 'numpy.float64'>'
with 9 stored elements in Compressed Sparse ... format>
>>> tfidf.toarray()
array([[ 0.81940995, 0. , 0.57320793],
[ 1. , 0. , 0. ],
[ 1. , 0. , 0. ],
[ 1. , 0. , 0. ],
[ 0.47330339, 0.88089948, 0. ],
[ 0.58149261, 0. , 0.81355169]])
各行は、単位ユークリッド標準を有するように正規化される:
v\_{norm} = \frac{v}{||v||\_2} = \frac{v}{\sqrt{v{\_1}^2 + v{\_2}^2 + \dots + v{_n}^2}}
たとえば、counts配列の最初の文書の最初の項のtf-idfを次のように計算できます。
n_{d, {\text{term1}}} = 6 \\
\text{df}(d, t)_{\text{term1}} = 6 \\
\text{idf}(d, t)_{\text{term1}} = log \frac{n_d}{\text{df}(d, t)} + 1 = log(1)+1 = 1 \\
\text{tf-idf}_{\text{term1}} = \text{tf} \times \text{idf} = 3 \times 1 = 3
ドキュメント内の残りの2つの項についてこの計算を繰り返すと、次のようになります。
\text{tf-idf}_{\text{term2}} = 0 \times log(6/1)+1 = 0 \\
\text{tf-idf}_{\text{term3}} = 1 \times log(6/2)+1 \approx 2.0986
生のtf-idfsのベクトル:
\text{tf-idf}_raw = [3, 0, 2.0986]
次に、ユークリッド(L2)ノルムを適用すると、文書1に対して以下のtf-idfsが得られる。
\frac{[3, 0, 2.0986]}{\sqrt{\big(3^2 + 0^2 + 2.0986^2\big)}}
= [ 0.819, 0, 0.573]
さらに、デフォルトパラメータsmooth_idf = True
は、分子と分母に「1」を追加します。余分な文書がコレクション内のすべての項を正確に1回含むように見えます。ゼロ割を防止します。
\text{idf}(t) = log{\frac{1 + n_d}{1+\text{df}(d,t)}} + 1
この変更を使用して、文書1の第3項のtf-idfが1.8473に変更されます。
\text{tf-idf}_{\text{term3}} = 1 \times log(7/3)+1 \approx 1.8473
そして、L2正規化されたtf-idfは
\frac{[3, 0, 1.8473]}{\sqrt{\big(3^2 + 0^2 + 1.8473^2\big)}} = [0.8515, 0, 0.5243]
>>> transformer = TfidfTransformer()
>>> transformer.fit_transform(counts).toarray()
array([[ 0.85151335, 0. , 0.52433293],
[ 1. , 0. , 0. ],
[ 1. , 0. , 0. ],
[ 1. , 0. , 0. ],
[ 0.55422893, 0.83236428, 0. ],
[ 0.63035731, 0. , 0.77630514]])
fit
メソッド呼び出しによって計算された各フィーチャの重みは、model属性に格納されます。
>>> transformer.idf_
array([ 1. ..., 2.25..., 1.84...])
tf-idfはテキスト機能に非常によく使用されるので、CountVectorizerとTfidfTransformerのすべてのオプションを単一のモデルにまとめるTfidfVectorizerという別のクラスもあります:
>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> vectorizer = TfidfVectorizer(min_df=1)
>>> vectorizer.fit_transform(corpus)
...
<4x9 sparse matrix of type '<... 'numpy.float64'>'
with 19 stored elements in Compressed Sparse ... format>
TF-IDF正規化は、しばしば非常に有用であるが、バイナリ発生マーカーが、より良い特徴を提供する例があるかもしれません。 これは、CountVectorizerのbinary
パラメータを使用することで実現できます。 特に、Bernoulli Naive Bayes のような推定器は、離散ブール確率変数を明示的にモデル化する。また、非常に短いテキストはtf-idfでノイズが多くなる可能性があり、一方、バイナリ発生情報はより安定している。
いつものように、特徴抽出パラメータを調整する最良の方法は、例えば、特徴抽出器を分類器でパイプライン化することによって、クロスバリデーションされたグリッドサーチを使用することである。
4.2.3.5. テキストファイルのデコード
テキストは文字で構成されていますが、ファイルはバイトで構成されています。これらのバイトは、いくつかのエンコーディングに従った文字を表します。 Pythonでテキストファイルを操作するには、そのバイトをUnicodeと呼ばれる文字セットにデコードする必要があります。一般的なエンコーディングはASCII、Latin-1(西ヨーロッパ)、KOI8-R(ロシア語)、ユニバーサルエンコーディングUTF-8とUTF-16です。他にも多くのものが存在します。
scikit-learnのテキスト特徴量抽出プログラムはテキストファイルのデコード方法を知っていますが、ファイルのエンコード方法を指定した場合にのみ表示されます。この目的のために、CountVectorizerはencoding
パラメータを取ります。現代のテキストファイルの場合、正しいエンコーディングはおそらくUTF-8であるため、デフォルト(encoding = "utf-8"
)です。
しかし、ロードしているテキストが実際にUTF-8でエンコードされていない場合は、UnicodeDecodeError
を取得します。デコードエラーについては、decode_errorパラメータを "ignore"
または "replace"
に設定することによって、ベクタライザを黙らせます。 Pythonプロンプトでhelp(bytes.decode)と入力し、Python関数 bytes.decode
のドキュメントを参照してください。
テキストのデコードに問題がある場合は、試してみてください:
- テキストの実際のエンコーディングが何であるかを調べる。ファイルには、エンコーディングを示すヘッダーまたはREADMEが付いている場合があります。また、テキストの出所に基づいて想定できる標準的なエンコーディングがある場合もあります。
- UNIXコマンド・ファイルを使用して、どのようなエンコーディングが一般的であるかを知ることができます。 Pythonのchardetモジュールには、特定のエンコーディングを推測する
chardetect.py
というスクリプトが付属していますが、正しいと推測することはできません。 - UTF-8を試してエラーを無視することもできます。
bytes.decode(errors = 'replace')
でバイト文字列をデコードして、すべてのデコードエラーを意味のない文字に置き換えたり、ベクタライザでdecode_error = 'replace'
を設定することができます。これにより、特徴量の有用性が損なわれる可能性があります。 - 実際のテキストは、さまざまなエンコーディングを使用している可能性のあるさまざまなソースから来るかもしれませんし、エンコーディングされたものとは異なるエンコーディングでうっかりデコードされるかもしれません。これは、Webから取得したテキストでは一般的です。 Pythonパッケージのftfyは自動的にいくつかのクラスのデコードエラーを並べ替えることができるので、未知のテキストをlatin-1としてデコードし、ftfyを使ってエラーを修正することができます。
- テキストが並べ替えが難しいエンコーディングのごたまぜの場合(20 Newsgroups データセットなど)、latin-1のような単純なシングルバイトエンコーディングに戻すことができます。一部のテキストは正しく表示されない場合がありますが、少なくとも同じシーケンスのバイトは常に同じ機能を表します。
たとえば、次のスニペットは、3つのテキストのエンコーディングを把握するためにchardet(scikit-learnと一緒には出荷されず、別途インストールする必要があります)を使用します。その後、テキストをベクトル化し、学習した語彙を印刷します。出力はここでは表示されません。
>>> import chardet
>>> text1 = b"Sei mir gegr\xc3\xbc\xc3\x9ft mein Sauerkraut"
>>> text2 = b"holdselig sind deine Ger\xfcche"
>>> text3 = b"\xff\xfeA\x00u\x00f\x00 \x00F\x00l\x00\xfc\x00g\x00e\x00l\x00n\x00 \x00d\x00e\x00s\x00 \x00G\x00e\x00s\x00a\x00n\x00g\x00e\x00s\x00,\x00 \x00H\x00e\x00r\x00z\x00l\x00i\x00e\x00b\x00c\x00h\x00e\x00n\x00,\x00 \x00t\x00r\x00a\x00g\x00 \x00i\x00c\x00h\x00 \x00d\x00i\x00c\x00h\x00 \x00f\x00o\x00r\x00t\x00"
>>> decoded = [x.decode(chardet.detect(x)['encoding'])
... for x in (text1, text2, text3)]
>>> v = CountVectorizer().fit(decoded).vocabulary_
>>> for term in v: print(v)
(chardet
のバージョンによっては、最初のものが間違っているかもしれません。)
Unicodeと文字エンコーディングの概要については、Joel Spolskyのすべてのソフトウェア開発者がUnicodeについて知っておく必要がある絶対最小値を参照してください。
4.2.3.6. アプリケーションとサンプル
Bag of Words 表現は非常に単純ですが、実際には驚くほど有用です。
特に 教師あり設定 では、 文書分類器 を訓練するために、高速でスケーラブルな線形モデルとうまく組み合わせることができます。
教師なし設定では、K-meansのようなクラスタリングアルゴリズムを適用することによって、類似したドキュメントをまとめてグループ化することができます。
最後に、非負行列分解(NMFまたはNNMF)を使用するなど、クラスタリングのハード割り当て制約を緩和することによって、コーパスの主要トピックを発見することが可能です。
4.2.3.7. Bag of Wordsの表現の限界
ユニグラムの集合(どの単語の袋であるか)は、フレーズや複数単語の表現を捕捉することができず、効果的に単語順序の依存性を無視します。さらに、Bag of Wordsモデルは、潜在的なスペルミスまたは単語の派生を説明しません。
救助にNグラム!ユニグラムの単純な集合(n = 1)を構築する代わりに、連続する単語の対の出現を数えるバイグラム(n = 2)の集合を好むかもしれない。
代わりに、文字Nグラムの集合、スペルミスや派生語に対する弾力性のある表現を考慮してもよい。
たとえば、2つの文書['words'、 'wprds']
のコーパスを扱っているとしましょう。 2番目の文書には単語「単語」のスペルミスが含まれています。単純なBag of Words表現は、これらの2つを非常に異なる文書とみなし、2つの可能な特徴の両方が異なる。しかし、文字2グラム表現は、8つの特徴のうち4つに一致する文書を見つけることができ、これは好ましい分類器がより良く決定するのを助けることができる。
>>> ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(2, 2), min_df=1)
>>> counts = ngram_vectorizer.fit_transform(['words', 'wprds'])
>>> ngram_vectorizer.get_feature_names() == (
... [' w', 'ds', 'or', 'pr', 'rd', 's ', 'wo', 'wp'])
True
>>> counts.toarray().astype(int)
array([[1, 1, 1, 0, 1, 1, 1, 0],
[1, 1, 0, 1, 1, 1, 0, 1]])
上記の例では、 char_wb
アナライザーが使用されています。これは、単語境界内の文字からのみnグラムを作成します(両側にスペースが埋め込まれています)。代わりに、 char
アナライザは単語にまたがるn-gramを作成します:
>>>
>>> ngram_vectorizer = CountVectorizer(analyzer='char_wb', ngram_range=(5, 5), min_df=1)
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
...
<1x4 sparse matrix of type '<... 'numpy.int64'>'
with 4 stored elements in Compressed Sparse ... format>
>>> ngram_vectorizer.get_feature_names() == (
... [' fox ', ' jump', 'jumpy', 'umpy '])
True
>>> ngram_vectorizer = CountVectorizer(analyzer='char', ngram_range=(5, 5), min_df=1)
>>> ngram_vectorizer.fit_transform(['jumpy fox'])
...
<1x5 sparse matrix of type '<... 'numpy.int64'>'
with 5 stored elements in Compressed Sparse ... format>
>>> ngram_vectorizer.get_feature_names() == (
... ['jumpy', 'mpy f', 'py fo', 'umpy ', 'y fox'])
True
単語の境界を意識した変種char_wb
は、単語分離のための空白を使用する言語で特に興味深いです。そのような言語の場合、スペルミスや単語の派生に関する堅牢性を保ちながら、そのような特徴を用いて訓練された分類器の予測精度と収束速度の両方を高めることができる。
個々の単語の代わりにnグラムを抽出することによっていくつかの語順を保存することができますが、Bag of Words や Bag of n-gram は文書の内部構造の大部分を破壊し、その内部構造によって運ばれる意味のほとんどを破壊します。
自然言語理解の幅広い課題に取り組むためには、文章と段落の局所構造を考慮する必要があります。そのようなモデルの多くは、現在、scikit-learn の対象外となっている「構造化されたアウトプット」の問題としてキャストされます。
4.2.3.8. ハッシュトリックを使用して大きなテキストコーパスをベクトル化する
上記のベクトル化スキームは単純ですが、文字列からインデックスへのマッピング(vocabulary_属性)をメモリー内に保持しているため、大きなデータセットを扱うときにいくつかの問題が発生します。
- コーパスが大きくなればなるほど、語彙が大きくなり、メモリ使用量も増えます。
- フィッティングには、元のデータセットのサイズに比例したサイズの中間データ構造の割り当てが必要です。
- 単語マッピングを構築するには、データセットを一旦全て読み込む必要があります。したがって、厳密にオンラインでテキスト分類器を適合させることはできません。
- 大きなボキャブラリを持つベクタライザのシリアライズ/デシリアライズは、非常に遅い(通常、同じサイズのNumPy配列などのフラットなデータ構造をシリアライズ/デシリアライズするよりもはるかに遅い)。
- vocabulary_属性がきめ細かい同期障壁を有するので、ベクトル化作業を並行したサブタスクに分割することは容易ではない。すなわち、トークン文字列から特徴インデックスへのマッピングは、それぞれの最初の出現順序したがってトークンが共有されなければならず、並列化のパフォーマンスを逐次処理より遅くする可能性があります。
sklearn.feature_extraction.FeatureHasher クラスによって実装された "ハッシングトリック"(フィーチャーハッシュ)と、CountVectorizerのテキスト前処理およびトークン化機能を組み合わせることによって、これらの制限を克服できます。
この組み合わせは、ほとんどがCountVectorizerと互換性のあるトランスクラスのHashingVectorizerで実装されています。 HashingVectorizerはステートレスです。つまり、それを呼び出す必要はありません。
>>> from sklearn.feature_extraction.text import HashingVectorizer
>>> hv = HashingVectorizer(n_features=10)
>>> hv.transform(corpus)
...
<4x10 sparse matrix of type '<... 'numpy.float64'>'
with 16 stored elements in Compressed Sparse ... format>
ベクトル出力では、16個の非ゼロの特徴トークンが抽出されていることがわかります。これは、同じ玩具コーパスのCountVectorizerによって以前に抽出された19個の非ゼロよりも小さくなります。この不一致は、n_features
パラメータの値が低いため、ハッシュ関数の衝突に起因します。
現実世界の設定では、n_features
パラメータはデフォルト値の2 ** 20
(約100万の可能な機能)のままにすることができます。メモリやダウンストリームモデルのサイズが問題の場合は、2^18
などのより低い値を選択することで、一般的なテキスト分類タスクにあまりに多くの追加の衝突を導入しなくても役立ちます。
次元数は、CSR行列で動作するアルゴリズム(LinearSVC(dual = True)
、Perceptron
、SGDClassifier
、PassiveAggressive
)のCPUトレーニング時間には影響しませんが、CSCマトリクスで動作するアルゴリズム(LinearSVC(dual=False)
, Lasso()
など)には影響することに注意してください。
デフォルト設定でもう一度試してみましょう:
>>> hv = HashingVectorizer()
>>> hv.transform(corpus)
...
<4x1048576 sparse matrix of type '<... 'numpy.float64'>'
with 19 stored elements in Compressed Sparse ... format>
>>>
私たちはもはや衝突を得ることはありませんが、これは出力空間のより大きな次元性を犠牲にしています。もちろん、ここで使用されている19以外の用語は、依然として互いに衝突する可能性があります。
HashingVectorizerには、次の制限もあります。
- マッピングを実行するハッシュ関数の一方向の性質のため、モデルを逆変換したり(
inverse_transform
メソッドなし)、特徴量の元の文字列表現にアクセスすることはできません。 - モデルが状態を持たないようにするため、IDFの重み付けは行われません。 TfidfTransformerは、必要に応じてパイプラインで追加することができます。
4.2.3.9. HashingVectorizerでアウトオブコアスケーリングを実行する
HashingVectorizer を使用する興味深い開発は、Out-of-Coreスケーリングを実行する能力です。つまり、コンピュータのメインメモリに収まらないデータから学ぶことができます。
Out-of-Coreスケーリングを実装する戦略は、データをミニバッチで推定器に流し込むことです。各ミニバッチは、推定器の入力空間が常に同じ次元を持つことを保証するために、HashingVectorizerを使用してベクトル化されます。したがって、いつでも使用されるメモリの量は、ミニバッチのサイズによって制限されます。このようなアプローチを使用して取り込むことができるデータの量に制限はありませんが、実用的な観点から、学習時間はしばしばタスクに費やすCPU時間によって制限されます。
テキスト分類タスクにおけるOut-of-Coreスケーリングの本格的な例については、テキスト文書のOut-of-Core分類 を参照してください。
4.2.3.10. ベクタライザクラスのカスタマイズ
callableをvectorizerコンストラクタに渡すことで、動作をカスタマイズすることができます。
>>> def my_tokenizer(s):
... return s.split()
...
>>> vectorizer = CountVectorizer(tokenizer=my_tokenizer)
>>> vectorizer.build_analyzer()(u"Some... punctuation!") == (
... ['some...', 'punctuation!'])
True
具体的には、
-
preprocessor
:ドキュメント全体を入力として(単一の文字列として)呼び出すことができ、変換されたドキュメントを返すcallableオブジェクトです。これは、HTMLタグを削除したり、文書全体を小文字にするために使用することができます。 -
tokenizer
:プリプロセッサからの出力をトークンに分割し、それらのリストを返すcallableオブジェクト。 -
analyzer
:プリプロセッサとトークナイザを置き換えるcallableオブジェクト。デフォルトのアナライザはすべてプリプロセッサとトークナイザを呼び出しますが、カスタムアナライザはこれをスキップします。 Nグラム抽出およびストップワードフィルタリングはアナライザレベルで行われるため、カスタムアナライザはこれらのステップを再現する必要があります。
(Luceneユーザーはこれらの名前を認識するかもしれませんが、scikit-learnの概念はLuceneの概念に一対一で対応していない可能性があることに注意してください)。
プリプロセッサ、トークナイザおよびアナライザがモデルパラメータを認識できるよう、カスタム関数を渡す代わりに、クラスから派生してbuild_preprocessor
、build_tokenizer
および build_analyzer
ファクトリメソッドをオーバーライドすることができます。
いくつかのヒントと秘訣:
- ドキュメントが外部パッケージによって事前にトークン化されている場合は、空白で区切られたトークンをファイル(または文字列)に格納し、
analyzer = str.split
を渡します - ステミング、字形分割、複合分割、品詞に基づくフィルタリングなどの素晴らしいトークンレベル解析は、scikit-learningコードベースには含まれていませんが、トークナイザまたはアナライザのいずれかをカスタマイズすることによって追加できます。ここに NLTK を使ったトークナイザとレマタイザを備えたCountVectorizerがあります:
>>> from nltk import word_tokenize
>>> from nltk.stem import WordNetLemmatizer
>>> class LemmaTokenizer(object):
... def __init__(self):
... self.wnl = WordNetLemmatizer()
... def __call__(self, doc):
... return [self.wnl.lemmatize(t) for t in word_tokenize(doc)]
...
>>> vect = CountVectorizer(tokenizer=LemmaTokenizer())
(これは句読点を除外しないことに注意してください)。
ベクタライザーのカスタマイズは、空白などの明示的な単語区切り文字を使用しないアジア言語を処理する場合にも役立ちます。
4.2.4. 画像特徴抽出
4.2.4.1. パッチ抽出
extract_patches_2 関数は、2次元配列として格納された画像、または3番目の軸に沿って色情報を持つ3次元パッチを抽出します。すべてのパッチからイメージを再構築するには、 reconstruct_from_patches_2d を使用します。例えば、3つのカラーチャネル(例えば、RGBフォーマット)を有する4×4ピクセル画像を生成するために使用する。
>>> import numpy as np
>>> from sklearn.feature_extraction import image
>>> one_image = np.arange(4 * 4 * 3).reshape((4, 4, 3))
>>> one_image[:, :, 0] # R channel of a fake RGB picture
array([[ 0, 3, 6, 9],
[12, 15, 18, 21],
[24, 27, 30, 33],
[36, 39, 42, 45]])
>>> patches = image.extract_patches_2d(one_image, (2, 2), max_patches=2,
... random_state=0)
>>> patches.shape
(2, 2, 2, 3)
>>> patches[:, :, :, 0]
array([[[ 0, 3],
[12, 15]],
[[15, 18],
[27, 30]]])
>>> patches = image.extract_patches_2d(one_image, (2, 2))
>>> patches.shape
(9, 2, 2, 3)
>>> patches[4, :, :, 0]
array([[15, 18],
[27, 30]])
オーバーラップ領域を平均化することによって、パッチから元の画像を再構成しようとしましょう。
>>> reconstructed = image.reconstruct_from_patches_2d(patches, (4, 4, 3))
>>> np.testing.assert_array_equal(one_image, reconstructed)
PatchExtractor クラスはextract_patches_2dと同じ方法で動作しますが、複数の画像を入力としてサポートしています。推定器として実装されているため、パイプラインで使用できます.
>>> five_images = np.arange(5 * 4 * 4 * 3).reshape(5, 4, 4, 3)
>>> patches = image.PatchExtractor((2, 2)).transform(five_images)
>>> patches.shape
(45, 2, 2, 3)
4.2.4.2. 画像の接続性グラフ
scikit-learnのいくつかの推定値は、特徴量またはサンプル間の接続情報を使用できます。例えば、ウォードクラスタリング(階層的クラスタリング)は、画像の隣接するピクセルのみをクラスタリングすることができ、したがって、連続したパッチを形成することができる。
この目的のために、推定器はどのサンプルが接続されているかを示す「接続性」マトリックスを使用する。
関数 img_to_graph は、2Dまたは3D画像からそのような行列を返します。同様に、 grid_to_graph は、これらの画像の形状が与えられた画像のための連結行列を構築する。
これらの行列は、ウォードクラスタリング(階層的クラスタリング)などの接続性情報を使用する推定器に接続性を課すために使用できます。また、事前計算されたカーネルまたは類似性行列を構築するためにも使用できます。
scikit-learn 0.18 ユーザーガイド 4. データセット変換 より
©2010 - 2016、scikit-learn developers(BSDライセンス)。