ããŒãã«ããŒã¿åãã®èªç¶èšèªç¹åŸŽæœåºè¡
ã¯ããã«
ãã®èšäºã¯ãèªç¶èšèªãå«ãã«ã©ã ãæã£ãããŒãã«ããŒã¿ã«ãããŠãæåž«ããåŠç¿çšã«ããã¹ãã®ã«ã©ã ãåºå®é·ã®ç¹åŸŽãã¯ãã«ã«çŽãæ¹æ³ããŸãšãããã®ã§ãã
äŸãšããŠãããããŒã¿ã¯å šãŠãatmaCup#10ã®ãã®ã§ãããŸãããã®èšäºã®å 容ã¯ãã¡ãã®ããŒãããã¯ã§å®éšãè¡ã£ãŠããŸãã
ããŒã¿ã®äŸã'title'ã'description'ãªã©èªç¶èšèªãå«ãã«ã©ã ãååšããã
åè: èªç¶èšèªåŠçã«ãããEmbeddingã®æ¹æ³äžèЧãšãµã³ãã«ã³ãŒã
Bag of WordsããŒã¹ã®ææ³
ææžãããŒã¯ã³ã®éåãšããŠæ±ãææ³ã§ããããŒã¯ã³ãšããŠã¯ããåèªãéžã°ããŸãããèªåã§tokenizerãèšå®ããŠæç« ãåèªä»¥å€ã®tokenã®éåãšããŠæ±ãããšãã§ããŸãããŸããæ¥æ¬èªãªã©ã®èšèªã«ãããŠã¯ããŒã¯ã³åãèªæã§ãªããããMeCabãªã©ãçšããŠããŒã¯ã³åããããšãããªãå€ãã§ãã
ã³ã©ã MeCabãçšããããŒã¯ã³å
MeCabã®ã€ã³ã¹ããŒã«æ¹æ³ã¯æ¬¡ã®èšäºãåèã«ãªããŸãã
ãã®äžã§æ¬¡ã®ãããªtokenizerãå®çŸ©ãããšæ¥æ¬èªã®æç« ãåãã¡æžããããåèªã®ãªã¹ãã«å€æããããšãã§ããŸãã
import MeCab
dict_path = "<MeCabã®èŸæžã®å Žæãæå®>"
tagger = MeCab.Tagger(dict_path)
def mecab_tokenizer(s: str):
parse_result = tagger.parse(s)
return [
result.split("\t")[0]
for result in parse_result.split("\n")
if result not in ["EOS", ""]
]
ãã®mecab_tokenizerãCountVectorizerãTfidfVectorizerãªã©ã®tokenizerãšããåŒæ°ã«æž¡ãããšã§MeCabãçšããããŒã¯ã³åãããäžã§Bag of WordsãäœæããŠãããŸãã
CountVectorizer
CountVectorizerã¯æç« äžã®tokenã®é »åºŠãæ°ããã¹ããŒã¹ãããªã¯ã¹ãäœæããŸããè¡åã®åè¡ãåæç« ã«è©²åœããååãtokenã«å¯Ÿå¿ããŸããã€ãŸããæç« ãããtokenãããããªããã§ç¹åŸŽã¥ãããã¯ãã«ãåŸãææ³ã§ããããã©ã«ãã®ãã€ããŒãã©ã¡ãŒã¿èšå®ã§ã¯æç« ãç©ºçœæå" "ã§åºåã£ãŠtokenåãããããè±èªã(ä»åã®äŸã§çšããŠããããŒã¿äžã«ãã)ãªã©ã³ãèªãªã©ã«ãããŠã¯åèªããã®ãŸãŸtokenã«ãªããŸãã
from sklearn.feature_extraction.text import CountVectorizer
cv = CountVectorizer()
features = cv.fit_transform(df["title"].fillna(""))
çµæãšããŠä»åã®ããŒã¿ã§ã¯æ¬¡ã®ãããªç¹åŸŽè¡åãåŸãããŸãããã®ã¹ããŒã¹ãããªã¯ã¹ãšããã®ã¯çãªå·šå€§è¡åãã¡ã¢ãªå¹çãã衚çŸã§ããããŒã¿æ§é ã§ããtoarray()ã§éåžžã®numpyã®é
åã«å€æãå¯èœã§ãããã¡ã¢ãªã倧ããæ¶è²»ããããæšå¥šãããŸããã
<12026x13851 sparse matrix of type '<class 'numpy.int64'>'
with 79165 stored elements in Compressed Sparse Row format>
features[0].toarray()
# => array([[0, 0, 0, ..., 0, 0, 0]])
scikit-learnã®å€ãã®ææ³ãLightGBMãªã©ã®æåã©ã€ãã©ãªã¯ã¹ããŒã¹ãããªã¯ã¹ãå ¥åãšããŠåãåãããã«ãªã£ãŠããã®ã§ç¹å¥ãªçç±ããªãéããã®ãŸãŸã«ããŠããã®ãããã§ãããã
ç¹åŸŽãã¯ãã«ã®ååã®ååãããªãã¡tokenåã¯CountVectorizerã®ã€ã³ã¹ã¿ã³ã¹ã®æ¹ã«ä¿åãããŠããŸãã
cv.get_feature_names()[:20]
# => ['02', '07', '10', '100', '11', '12',...]
ã³ã©ã fitãtransformãfit_transformã®ååšæçŸ©ã«ã€ããŠ
ãããŸã§ã®æµãã§ãfit_transformãšããã¡ãœãããåºãŠããŸããããããã¯scikit-learnã«å®è£
ãããŠããTransformerã§ããããã¡ãœããã§ãããã®ã¡ãœãããããã¯ã©ã¹ã«ã¯ä»ã«fitãštransformãšããã¡ãœãããå®è£
ãããŠããŸãã
ãã®ã³ã©ã ã§ã¯fitãtransformãfit_transformã®ååšæçŸ©ã«ã€ããŠèª¬æããŸãã
fitã¡ãœãããštransformã¡ãœãããããTransformerã®å€ãã¯å
éšã«ããŒã¿ã®å€æã®ããã®"èŸæž"ã®ãããªãã®ãæã£ãŠããŸãããã®"èŸæž"ã¯åæç¶æ
ã§ã¯ãŸã£ããã§ããã倿ã®éã®å¯Ÿå¿é¢ä¿ãfitã¡ãœããã䜿ã£ãŠåŠç¿ããtransformã¡ãœããã䜿ã£ãŠããŒã¿ã«å€æèŠåãé©çšããããšããæµãã«ãªã£ãŠããŸãããã®äžé£ã®æµããã²ãšãŸãšãã«ãããã®ãfit_transformã¡ãœããã§ãã
æ©æ¢°åŠç¿ã§ã¯ãåŠç¿ããŒã¿ã§åŠç¿ããã¢ãã«ããã¹ãããŒã¿ã«å¯ŸããŠé©çšããããšããæµãããããŸããããã¯(äžè¬ã«ã¯ã¢ãã«ã«å«ãŸããªã)ååŠçã»ç¹åŸŽæœåºã®éã®å€æèŠåã«é¢ããŠãåãã§ããåŠç¿ããŒã¿ã«å¯ŸããŠfitããŠ"èŸæž"ã®åŠç¿ãè¡ã£ãäžã§åŠç¿ããŒã¿ã»ãã¹ãããŒã¿ã«å¯ŸããŠtransformã§å€æèŠåã®é©çšãè¡ãããšããã®ãäžé£ã®æµããšãªããŸãã
äŸãšããŠCountVectorizerã§ã¯ã©ã®ãããª"èŸæž"ãfitã«ãã£ãŠäœæãããã®ããã¿ãŠã¿ãŸãããã
cv = CountVectorizer()
# fitåã®ã¢ããªãã¥ãŒã
pre_fit_attributes = set(dir(cv))
cv.fit(df["title"].fillna(""))
# fitåŸã®ã¢ããªãã¥ãŒã
post_fit_attributes = set(dir(cv))
post_fit_attributes - pre_fit_attributes
# => {'_stop_words_id', 'fixed_vocabulary_', 'stop_words_', 'vocabulary_'}
_stop_words_id, fixed_vocabulary_, stop_words_, vocabulary_ãšããã¢ããªãã¥ãŒããfitã¡ãœãããèªãã åŸã«ã¯ã©ã¹ã«è¶³ãããŠããããšãããããŸãããã®äžã§ãç¹ã«éèŠãªã®ãvocabulary_ã§ããã¿ãŠã¿ãŸãããã
type(cv.vocabulary_), len(cv.vocabulary_)
# => (dict, 13851)
list(cv.vocabbulary_.key())[:10]
# => ['the', 'avenue', 'of', 'birches', 'struik', 'in', 'bloei', 'portret', 'van', 'een']
cv.vocabulary_["the"]
# => 11815
CountVectorizerã§ã¯æåéãèŸæžãfitã«ãã£ãŠäœæãããŸãããã®èŸæžã¯åèªåœ(token)ãšãã®åºçŸåæ°ãèšé²ãããã®ã«ãªã£ãŠããŸãããã®èŸæžãå
ã«æç« ããã®åèªã®åºçŸåæ°ã«èšé²ãããã®ã«ãªã£ãŠããŸãããã®èŸæžãå
ã«æç« ããã®åèªã®åºçŸåæ°ã«ãã£ãŠç¹åŸŽã¥ããã®ãtransformã¡ãœããã§ãã
scikit-learnã®Transformerã®äžã«ã¯ãLabelEncoderã®ããã«fitæã«ãªãã£ãã¯ã©ã¹ãé©çšæã«çŸãããšãšã©ãŒãèµ·ãããã®ããããŸãããCountVectorizerãTfidfVectorizerã¯ãã®å¿é
ããããŸããã
ããŸãããè¡ãããããšã§ã¯ãããŸããããfitãããããã¹ãã®ã«ã©ã ã§è¡ã£ãŠtransformãå¥ã®ã«ã©ã ã«å¯ŸããŠè¡ãããšèšã£ããããªããšãå¯èœã§ãã
TfidfVectorizer
TfidfVectorizerã¯åèªã®æ°ãäžããè¡ã£ãåŸãTfidfãšããææ³ã§éã¿ã¥ããããã¹ããŒã¹ãããªã¯ã¹ãäœæããŸããäœ¿ãæ¹ã¯CountVectorizerãšåæ§ã§ãã
from sklearn.feature_extraction.text import TfidfVectorizer
tv = TfidfVectorizer()
features = tv.fit_transform(df["title"].fillna(""))
ãã®çµæã次ã®ãããªã¹ããŒã¹ãããªã¯ã¹ãåŸãããŸã
<12026x13851 sparse matrix of type '<class 'numpy.float64'>'
with 79165 stored elements in Compressed Sparse Row format>
äžã®CountVectorizerã®äŸãšèŠæ¯ã¹ããšsparse matrix of type '<class 'numpy.int64'>'ããsparse matrix of type '<class 'numpy.float64'>'ã«å€ãã£ãŠããããšãããããŸãã
äžæ¹ã§79165 stored elementsãšããéšåã¯å€åããŠããŸãããããã¯ãé0ã®èŠçŽ ã®æ°ã衚ããŠããã®ã§ãããTfidfã¯CountVectorizerã®çµæã«éã¿ã¥ããè¡ã£ãã ãã®ãã®ã®ãã0ã®èŠçŽ ã¯éã¿ã¥ãããããŠã0ãšããŠæ®ããçµæãšããŠé0ã®èŠçŽ æ°ã¯å€åããªãããšã«ãããã®ã§ãã
BM25
Tfidfã®åé¡ç¹ãšããŠææžé·ãé·ããšå€§ããªå€ãã€ããããããšãããã®ããããŸããOkapi BM25ã¯ãã®åé¡ç¹ã解決ããããã«éã¿ã¥ãã®éã«ææžé·ã«ããè£æ£ããããŸãã
BM25ã®å®è£ ã¯scikit-learnã«ã¯ãªããããBM25Transformerã«ãããå®è£ ãåèã«ããŸããã
ã©ã€ã»ã³ã¹ãšå®è£
LICENSE
BSD 3-Clause License
Copyright (c) 2018, Sho IIZUKA All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import numpy as np
import scipy.sparse as sp
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.utils.validation import check_is_fitted
from sklearn.feature_extraction.text import _document_frequency
# reference: https://github.com/arosh/BM25Transformer/blob/master/bm25.py
class BM25Transformer(BaseEstimator, TransformerMixin):
"""
Parameters
----------
use_idf : boolean, optional (default=True)
k1 : float, optional (default=2.0)
b : float, optional (default=0.75)
References
----------
Okapi BM25: a non-binary model - Introduction to Information Retrieval
http://nlp.stanford.edu/IR-book/html/htmledition/okapi-bm25-a-non-binary-model-1.html
"""
def __init__(self, use_idf=True, k1=2.0, b=0.75):
self.use_idf = use_idf
self.k1 = k1
self.b = b
def fit(self, X):
"""
Parameters
----------
X : sparse matrix, [n_samples, n_features]
document-term matrix
"""
if not sp.issparse(X):
X = sp.csc_matrix(X)
if self.use_idf:
n_samples, n_features = X.shape
df = _document_frequency(X)
idf = np.log((n_samples - df + 0.5) / (df + 0.5))
self._idf_diag = sp.spdiags(idf, diags=0, m=n_features, n=n_features)
return self
def transform(self, X, copy=True):
"""
Parameters
----------
X : sparse matrix, [n_samples, n_features]
document-term matrix
copy : boolean, optional (default=True)
"""
if hasattr(X, 'dtype') and np.issubdtype(X.dtype, np.float):
# preserve float family dtype
X = sp.csr_matrix(X, copy=copy)
else:
# convert counts or binary occurrences to floats
X = sp.csr_matrix(X, dtype=np.float64, copy=copy)
n_samples, n_features = X.shape
# Document length (number of terms) in each row
# Shape is (n_samples, 1)
dl = X.sum(axis=1)
# Number of non-zero elements in each row
# Shape is (n_samples, )
sz = X.indptr[1:] - X.indptr[0:-1]
# In each row, repeat `dl` for `sz` times
# Shape is (sum(sz), )
# Example
# -------
# dl = [4, 5, 6]
# sz = [1, 2, 3]
# rep = [4, 5, 5, 6, 6, 6]
rep = np.repeat(np.asarray(dl), sz)
# Average document length
# Scalar value
avgdl = np.average(dl)
# Compute BM25 score only for non-zero elements
data = X.data * (self.k1 + 1) / (X.data + self.k1 * (1 - self.b + self.b * rep / avgdl))
X = sp.csr_matrix((data, X.indices, X.indptr), shape=X.shape)
if self.use_idf:
check_is_fitted(self, '_idf_diag', 'idf vector is not fitted')
expected_n_features = self._idf_diag.shape[0]
if n_features != expected_n_features:
raise ValueError("Input has n_features=%d while the model"
" has been trained with n_features=%d" % (
n_features, expected_n_features))
# *= doesn't work
X = X * self._idf_diag
return X
äžã®å®è£ ã¯CountVectorizerã®çµæã«å¯ŸããŠèšç®ãè¡ããã®ã®ãããæ¬¡ã®ããã«scikit-learnã®PipelineãçšããŠåŠçãç¹ããŸãã
from sklearn.pipeline import Pipeline
bm25 = Pipeline(steps=[
("CountVectorizer", CountVectorizer()),
("BM25Transformer", BM25Transformer())
])
features = bm25.fit_transform(df["title"].fillna(""))
features
# => <12026x13851 sparse matrix of type '<class 'numpy.float64'>'
# => with 79165 stored elements in Compressed Sparse Row format>
SVD/NMF/LDAã«ããå§çž®
ãããŸã§ã«ç޹ä»ããCountVectorizer, TfidfVectorizer, BM25ã®åºåã¯ããããæ°äžæ¬¡å ãè¶ ãã巚倧ãªè¡åã§ãããã®ãããªé«æ¬¡å ã®ããŒã¿ããåŠç¿ããããšã¯äžè¬çã«ã¯å°ãé£ãããšãããæ¬¡å åæžã®ææ³ãçšããŠäœæ¬¡å ã«çŽããããŒã¿ã§åŠç¿ãè¡ãããšããã°ãã°ãããã玹ä»ããŸãã
sklearn.decompositionã«ã¯ãããããªã¢ã«ãŽãªãºã ãå®è£
ãããŠããŸãããããçšããããã®ã¯TruncatedSVDãNMFãLatentDirichletAllocationã®ïŒçš®ã§ãã
Tfidfã®åºåã«TruncatedSVDãé©çšããŠã¿ãŸãããã
from sklearn.decomposition import TruncatedSVD, NMF, LatentDirichletAllocation
tfidf_svd = Pipeline(steps=[
("TfidfVectorizer", TfidfVectorizer()),
("TruncatedSVD", TruncatedSVD(n_components=50, random_state=42))
])
features_svd = tfidf_svd.fit_transform(df["title"].fillna(""))
features_svd
# => array([[ 0.00941832, 0.03515622, 0.27475458, ..., 0.00723061,
# => -0.00529586, -0.00076525],
# => [ 0.02541036, 0.05760959, 0.01006472, ..., -0.00457558,
# => 0.00829346, 0.00868814],
# => [ 0.76085596, -0.21396575, -0.07083936, ..., 0.01899481,
# => 0.031445 , 0.03774812],
# => ...,
# => [ 0.08828213, 0.18706189, -0.01792205, ..., 0.02066624,
# => -0.00753736, 0.01364502],
# => [ 0.06457228, 0.07335827, -0.0081457 , ..., -0.00264008,
# => -0.01883034, 0.01379052],
# => [ 0.01181844, 0.05063674, 0.39794632, ..., 0.02276429,
# => -0.01311549, 0.0044951 ]])
ä»åºŠã¯numpyã®å¯ãªé åã«ãªã£ãŠããããšãããããŸãã
features_svd.shape
# => (12026, 50)
ä»ã®äºã€ã®ã¢ã«ãŽãªãºã ã§ãåæ§ã®ãã€ãã©ã€ã³ã§ãã¯ãã«åããŠã¿ãŸãããã
tfidf_nmf = Pipeline(steps=[
("TfidfVectorizer", TfidfVectorizer()),
("NMF", NMF(n_components=50, random_state=42))
])
tfidf_lda = Pipeline(steps=[
("TfidfVectorizer", TfidfVectorizer()),
("LDA", LatentDirichletAllocation(n_components=50, random_state=42))
])
features_nmf = tfidf_nmf.fit_transform(df["title"].fillna(""))
features_lda = tfidf_lda.fit_transform(df["title"].fillna(""))
ããããã®åºåçµæ
features_nmf
array([[0.00000000e+00, 0.00000000e+00, 5.17095498e-02, ...,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
[2.43097678e-05, 0.00000000e+00, 0.00000000e+00, ...,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
[1.72472570e-01, 0.00000000e+00, 0.00000000e+00, ...,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
...,
[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, ...,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
[0.00000000e+00, 0.00000000e+00, 4.21709211e-02, ...,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00]])
features_lda
array([[0.00708276, 0.00708276, 0.00708276, ..., 0.00708276, 0.00708276,
0.00708276],
[0.0077113 , 0.0077113 , 0.0077113 , ..., 0.0077113 , 0.0077113 ,
0.0077113 ],
[0.00683002, 0.00683002, 0.00683002, ..., 0.00683002, 0.00683002,
0.00683002],
...,
[0.00533757, 0.00533757, 0.00533757, ..., 0.00533757, 0.00533757,
0.00533757],
[0.00623906, 0.00623906, 0.00623906, ..., 0.00623906, 0.00623906,
0.00623906],
[0.00684387, 0.00684387, 0.00684387, ..., 0.00684387, 0.00684387,
0.00684387]])
features_nmf.shape, features_lda.shape
# => ((12026, 50), (12026, 50))
ããããçµæã®é åã®åœ¢ã¯åãã§ãããèŠçŽ ã¯å€§ããç°ãªãããšãããããŸããç°ãªãã¢ã«ãŽãªãºã ã§æ¬¡å åæžãè¡ã£ãŠããããã§ãã
ãã®ãããªæ¬¡å åæžã¯TfidfVectorizerãCountVectorizerãBM25以å€ã«ããã®ä»ã®ããŸããŸãªãã¯ãã«è¡šçŸã®ææ³ã«å¯ŸããŠé©çšããããšãã§ããŸãã
åèªãã¯ãã«è¡šçŸãçšããæ¹æ³
Word2VecãGloVeãfastTextãªã©ã®åèªãã¯ãã«è¡šçŸãçšããæç« ãã¯ãã«äœæææ³ã«ã€ããŠã§ãã
fastTextã®æç« ãã¯ãã«
fastTextã®å
¬åŒå®è£
ã«ã¯ãget_sentence_vectorãšããAPIãçšæãããŠãããã¢ãã«ãèªã¿èŸŒãã ãã§ããã«äœ¿ãããšãã§ããŸãã
ä»åã¯äœ¿çšããããŒã¿ããªã©ã³ãèªã®ããããªã©ã³ãèªã®fastTextã®äºååŠç¿æžã¿ã¢ãã«ãèªã¿èŸŒãã§ã¿ãŸãã
import fasttext
import fasttext.util
fasttext.util.download_model('nl', if_exists='ignore')
ft_model = fasttext.load_model('cc.nl.300.bin')
fastTextã«ã¯ãå
¬åŒã®ããŒãžã«ãã157èšèªã®åŠç¿æžã¿åèªåæ£è¡šçŸãããŠã³ããŒãããŠå±éãããŠãŒãã£ãªãã£ãçšæãããŠããŸããäžã®ã³ãŒãã¯ãã®äŸã§ãããªããåŠç¿æžã¿åæ£è¡šçŸã¯7GBè¶
ããããããªãéããããŠã³ããŒãã«ãããªãã®æéããããã»ããèªã¿èŸŒã¿ã®éã«ã¡ã¢ãªãå€ãæ¶è²»ãããããšãåé¡ç¹ãšããŠãããŸãã
èªã¿èŸŒãã fastTextã®ã¢ãã«ã§æç« ãã¯ãã«ãåºããŠã¿ãŸããããæ¬¡ã®ãããªãªã©ã³ãèªã®æç« ãåãæ±ã£ãŠã¿ãŸãããã
title = df["title"].loc[100]
title
# => 'Een brug over een kloof in het Gotthard gebergte'
features = ft_model.get_sentence_vector(title)
features
# => array([ 2.15894822e-02, 1.24604534e-02, -1.04461201e-02, 7.22003169e-03,
# => 2.98381317e-02, -3.92103428e-03, -3.29393856e-02, -1.28988596e-02,
# => 8.84584989e-03, -1.39617221e-02, -2.93837693e-02, -1.26295292e-03,
# => 2.34530065e-02, 5.40923653e-03, 1.79306362e-02, -2.60739867e-02,
# => -2.39493661e-02, 2.20047366e-02, 3.33243906e-02, -4.48094215e-03,
# => ...], dtype=float32)
features.shape
# => (300,)
æç« äžã€ã«å¯Ÿãã300次å ã®ãã¯ãã«ãåºåãããŠããããšãããããŸãããããå šãŠã®æç« ã«å¯ŸããŠåºããŠã¿ãŸãããã
features = np.stack(
df["title"].fillna("").str.replace("\n", "").map(
lambda x: ft_model.get_sentence_vector(x)
).values
)
features.shape
# => (12026, 300)
åŠç¿æžã¿Word2Vec / GloVeã®åèªãã¯ãã«ã®éçŽ
Word2VecãGloVeã¯fastText以åã«ææ¡ãããåèªãã¯ãã«è¡šçŸã§ãããçŸåšã®BERTå šçã®æä»£ã«ãããŠãã¿ã¹ã¯ã«ãã£ãŠã¯ããªãåŒ·ãææ³ã§ãã
Word2Vec / GloVeèªäœã¯åèªã®ãã¯ãã«è¡šçŸã§ãããæç« ãªãã®åèªããããããã¯ãã«è¡šçŸã«çœ®ãæããäžã§å次å ã®å¹³åããšã£ãããSWEMãSCDVãªã©ã®ææ³ã§æç« ãã¯ãã«ãåŸãããšãã§ããŸãã
ããã§ã¯ãŸããWord2Vecã®ãã¯ãã«è¡šçŸãèªã¿èŸŒãã§ããã®å¹³åãåãããæ¹ã§æç« ãã¯ãã«ãåŸãæ¹æ³ã玹ä»ããŸãã
ä»åã¯é¡æã察象ã®èšèªããªã©ã³ãèªãªã®ã§ãªã©ã³ãèªã®Word2Vec embeddingãclips/dutchembeddingsããåã£ãŠããŸãããªããè±èªãšãã·ã¢èªã®embeddingã§ããã°gensimã®downloader APIãçšããŠæ¬¡ã®ããã«åã£ãŠããããšãã§ããŸãã
import gensim
model = gensim.downloader('glove-twitter-25')
ãã®APIã§å ¥æå¯èœãªãªãœãŒã¹ã®ãªã¹ãã¯RaRe-Technologies/gensim-dataã«ãããŸãã
# ãªã©ã³ãèªã®embeddingãåã£ãŠãã
!wget https://onyx.uvt.nl/sakuin/_public/embeddings/wikipedia-320.tar.gz
!tar -zxvf wikipedia-320.tar.gz
!rm -f wikipedia-320.tar.gz
ããŠã³ããŒãããWord2Vecã®ã¢ãã«ãèªã¿èŸŒã¿ããªã©ã³ãèªã®æç« ãåèªã«åè§£ããäžã§ãåèªããšã«ãã¯ãã«ã«å€æããŠã¿ãŸãããããã ããæç« äžã®åèªãã¢ãã«ã«ãªãã£ãå Žå(out-of-vocabulary)ã¯ãŒããã¯ãã«ã«å€æããããšã«ããŸãã
from gensim.models import KeyedVectors
w2v_model = KeyedVectors.load_word2vec_format(
"./320/wikipedia-320.txt", binary=False)
embeddings = [
w2v_model.get_vector(word)
if w2v_model.vocab.get(word) is not None
else np.zeros(320)
for word in txt.split()
]
embeddings
äžã®ããæ¹ã«ãã£ãŠåæç« ã«å¯Ÿããæç« äžã®åèªæ°åã®ãã¯ãã«ã䞊ãã ãªã¹ããã§ããŸããæãåçŽãªããæ¹ã§ã¯ãã¯ãã«ã®å¹³åãåãããšã§æç« ãã¯ãã«ãåŸãããšãã§ããŸãã
ãã®ããæ¹ã§ãDataFrameäžã®å šæç« ãæç« ãã¯ãã«ã«å€æããŠã¿ãŸãããã
def get_sentence_vector(x: str, ndim=320):
embeddings = [
w2v_model.get_vector(word)
if w2v_model.vocab.get(word) is not None
else np.zeros(ndim, dtype=np.float32)
for word in x.split()
]
if len(embeddings) == 0:
return np.zeros(ndim, dtype=np.float32)
else:
return np.mean(embeddings, axis=0)
features = np.stack(
df["title"].fillna("").str.replace("\n", "").map(
lambda x: get_sentence_vector(x)
).values
)
features
# => array([[ 0.0218195 , -0.00529125, 0.00676325, ..., -0.014701 , 0.00603925, 0.00937075],
# => [-0.00052267, -0.00186367, 0.00838467, ..., -0.03165 , 0.02557333, -0.00187267],
# => [ 0.00350125, 0.0229975 , 0.02975875, ..., -0.00457425, -0.02403375, -0.01494225],
# => ...,
# => [ 0.0228829 , 0.0169449 , -0.0143778 , ..., -0.0023923 , -0.0042892 , 0.0246226 ],
# => [ 0.00500967, 0.01275717, 0.0063095 , ..., -0.0127645 , -0.00038867, 0.01078233],
# => [ 0.0192264 , -0.0115158 , 0.011208 , ..., -0.0417188 , 0.0116854 , -0.0011884 ]], dtype=float32)
features.shape
# => (12026, 320)
SWEM
åã®ç¯ã§ç޹ä»ããåèªãã¯ãã«ã®å¹³åãåãããšã§æç« ãã¯ãã«ãåŸãæ¹æ³ã¯SWEMãšåŒã°ããææ³çŸ€ã®äžã®äžã€ã®SWEM-averãšåŒã°ãããã®ã«ãªããŸãã
ãã®ä»ã«ã忬¡å ã«ã€ããŠmax poolingããšãSWEM-maxãSWEM-averãšSWEM-maxãäž¡æ¹èšç®ããäžã§çµåããSWEM-concatããããŠåºå®é·ã®çªå¹ ã®ãªãã§å次å ã®å¹³åãåã£ãåŸãmax poolingããšãSWEM-hierããããŸãã
ãããå šãŠã®å®è£ ãyagays/swemã§è¡ãããŠããã®ã§ãåãããŸãã
SWEMã®å®è£
class SimpleTokenizer:
def tokenize(self, text: str):
return text.split()
class SWEM():
"""
Simple Word-Embeddingbased Models (SWEM)
https://arxiv.org/abs/1805.09843v1
"""
def __init__(self, w2v, tokenizer, oov_initialize_range=(-0.01, 0.01)):
self.w2v = w2v
self.tokenizer = tokenizer
self.vocab = set(self.w2v.vocab.keys())
self.embedding_dim = self.w2v.vector_size
self.oov_initialize_range = oov_initialize_range
if self.oov_initialize_range[0] > self.oov_initialize_range[1]:
raise ValueError("Specify valid initialize range: "
f"[{self.oov_initialize_range[0]}, {self.oov_initialize_range[1]}]")
def get_word_embeddings(self, text):
np.random.seed(abs(hash(text)) % (10 ** 8))
vectors = []
for word in self.tokenizer.tokenize(text):
if word in self.vocab:
vectors.append(self.w2v[word])
else:
vectors.append(np.random.uniform(self.oov_initialize_range[0],
self.oov_initialize_range[1],
self.embedding_dim))
return np.array(vectors)
def average_pooling(self, text):
word_embeddings = self.get_word_embeddings(text)
return np.mean(word_embeddings, axis=0)
def max_pooling(self, text):
word_embeddings = self.get_word_embeddings(text)
return np.max(word_embeddings, axis=0)
def concat_average_max_pooling(self, text):
word_embeddings = self.get_word_embeddings(text)
return np.r_[np.mean(word_embeddings, axis=0), np.max(word_embeddings, axis=0)]
def hierarchical_pooling(self, text, n):
word_embeddings = self.get_word_embeddings(text)
text_len = word_embeddings.shape[0]
if n > text_len:
raise ValueError(f"window size must be less than text length / window_size:{n} text_length:{text_len}")
window_average_pooling_vec = [np.mean(word_embeddings[i:i + n], axis=0) for i in range(text_len - n + 1)]
return np.max(window_average_pooling_vec, axis=0)
tokenizer = SimpleTokenizer()
swem = SWEM(w2v=w2v_model, tokenizer=tokenizer)
ããããæ¬¡ã®ããã«è©Šãããšãã§ããŸãã
# SWEM-aver
# äžã§ç޹ä»ãããã®ãšåããã®
swem.average_pooling(txt)
# => array([-8.88644345e-03, -1.91837773e-02, -1.40529983e-02, -4.22796682e-02,
# => 1.32464431e-02, 3.00016664e-02, -8.08466598e-03, -3.73876654e-02,
# => -7.33510964e-03, 1.08800018e-02, -2.55460013e-02, 1.00997780e-02,
# => ...], dtype=float32)
# max pooling
swem.max_pooling(txt)
# SWEM concat
swem.concat_average_max_pooling(txt)
# SWEM-hier
swem.hierarchical_pooling(txt, n=3)
SCDV
SCDVã¯åèªãã¯ãã«ãããæãã«çŽããŠãããææ³ã§ãã
SCDVã«é¢ããŠã¯ããŸããŸãªããã°èšäºãæžãããŠããã®ã§ãã¡ããåç §ãããšãããããããŸãããå®è£ ã¯nyk510/scdv-pythonãåèã«ããŸããã
æç« ã®åã蟌ã¿ã¢ãã«: Sparse Composite Document Vectors ãèªãã§å®è£
ããŠã¿ã
è«æã¡ã¢ SCDV : Sparse Composite Document Vectors using soft clustering over distributional representations
SCDVã®å®è£
from sklearn.base import TransformerMixin, BaseEstimator
from sklearn.mixture import GaussianMixture
class SCDVEmbedder(TransformerMixin, BaseEstimator):
def __init__(self, w2v, tokenizer, k=5):
self.w2v = w2v
self.vocab = set(self.w2v.vocab.keys())
self.tokenizer = tokenizer
self.k = k
self.topic_vector = None
self.tv = TfidfVectorizer(tokenizer=self.tokenizer)
def __assert_if_not_fitted(self):
assert self.topic_vector is not None, \
"SCDV model has not been fitted"
def __create_topic_vector(self, corpus: pd.Series):
self.tv.fit(corpus)
self.doc_vocab = set(self.tv.vocabulary_.keys())
self.use_words = list(self.vocab & self.doc_vocab)
self.use_word_vectors = np.array([
self.w2v[word] for word in self.use_words])
w2v_dim = self.use_word_vectors.shape[1]
self.clf = GaussianMixture(
n_components=self.k,
random_state=42,
covariance_type="tied")
self.clf.fit(self.use_word_vectors)
word_probs = self.clf.predict_proba(
self.use_word_vectors)
world_cluster_vector = self.use_word_vectors[:, None, :] * word_probs[
:, :, None]
doc_vocab_list = list(self.tv.vocabulary_.keys())
use_words_idx = [doc_vocab_list.index(w) for w in self.use_words]
idf = self.tv.idf_[use_words_idx]
topic_vector = world_cluster_vector.reshape(-1, self.k * w2v_dim) * idf[:, None]
topic_vector = np.nan_to_num(topic_vector)
self.topic_vector = topic_vector
self.vocabulary_ = set(self.use_words)
self.ndim = self.k * w2v_dim
def fit(self, X, y=None):
self.__create_topic_vector(X)
def transform(self, X):
tokenized = X.fillna("").map(lambda x: self.tokenizer(x))
def get_sentence_vector(x: list):
embeddings = [
self.topic_vector[self.use_words.index(word)]
if word in self.vocabulary_
else np.zeros(self.ndim, dtype=np.float32)
for word in x
]
if len(embeddings) == 0:
return np.zeros(self.ndim, dtype=np.float32)
else:
return np.mean(embeddings, axis=0)
return np.stack(
tokenized.map(lambda x: get_sentence_vector(x)).values
)
def tokenizer(x: str):
return x.split()
scdv = SCDVEmbedder(w2v_model, tokenizer=tokenizer, k=5)
scdv.fit(df["title"])
features = scdv.transform(df["title"])
features.shape
# => (12026, 1600)
BERTãçšããæ¹æ³
è¿å¹Žã¯èªç¶èšèªåŠçã«å¯Ÿããç¹åŸŽæœåºæ³ãšããŠBERTãããã¡ã¯ãã¹ã¿ã³ããŒããšèšã£ãŠãå·®ãæ¯ããªãã§ããããããã§ã¯ãäºååŠç¿æžã¿ BERT ã¢ãã«ã䜿ã£ãããã¹ãç¹åŸŽæœåºã«ã€ããŠã§å ±æãããŠããæ¹æ³ã玹ä»ããŸãã
import torch
import transformers
from transformers import BertTokenizer
class BertSequenceVectorizer:
def __init__(self, model_name="bert-base-uncased", max_len=128):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.model_name = model_name
self.tokenizer = BertTokenizer.from_pretrained(self.model_name)
self.bert_model = transformers.BertModel.from_pretrained(self.model_name)
self.bert_model = self.bert_model.to(self.device)
self.max_len = max_len
def vectorize(self, sentence: str) -> np.array:
inp = self.tokenizer.encode(sentence)
len_inp = len(inp)
if len_inp >= self.max_len:
inputs = inp[:self.max_len]
masks = [1] * self.max_len
else:
inputs = inp + [0] * (self.max_len - len_inp)
masks = [1] * len_inp + [0] * (self.max_len - len_inp)
inputs_tensor = torch.tensor([inputs], dtype=torch.long).to(self.device)
masks_tensor = torch.tensor([masks], dtype=torch.long).to(self.device)
bert_out = self.bert_model(inputs_tensor, masks_tensor)
seq_out, pooled_out = bert_out['last_hidden_state'], bert_out['pooler_output']
if torch.cuda.is_available():
return seq_out[0][0].cpu().detach().numpy() # 0çªç®ã¯ [CLS] token, 768 dim ã®æç« ç¹åŸŽé
else:
return seq_out[0][0].detach().numpy()
BSV = BertSequenceVectorizer(
model_name="bert-base-multilingual-uncased",
max_len=128)
features = np.stack(
df["title"].fillna("").map(lambda x: BSV.vectorize(x).reshape(-1)).values
)
features
# => array([[ 0.05518435, -0.01929706, -0.00717838, ..., 0.05105488, -0.01119471, 0.02306907],
# => [-0.01522179, -0.0586175 , -0.03396389, ..., -0.02157267, -0.08532659, 0.00154577],
# => [-0.06408089, 0.01533521, -0.07290605, ..., -0.00512743, -0.08937339, -0.00232941],
# => ...,
# => [-0.02091743, 0.02608643, -0.04621095, ..., 0.12933932, -0.03479976, 0.04011173],
# => [ 0.04437887, -0.00519709, -0.00229898, ..., 0.01011661, -0.02555398, 0.03275691],
# => [-0.01799066, 0.02379747, -0.06540792, ..., 0.05809147, -0.03211721, 0.03436833]], dtype=float32)
features.shape
# => (12026, 768)
Universal Sentence Encoderãçšããæ¹æ³
æç« ãã¯ãã«ãåãæ¹æ³ãšããŠãUniversal Sentence EncoderãšåŒã°ããã¢ãã«ããããŸããUniversal Sentece Encoderã¯ç°ãªãèšèªã§ããæè»œã«åããã¯ãã«ç©ºéäžã«åã蟌ãããšãã§ããææ³ãšããŠéåžžã«äœ¿ãåæããããšèšãããŠããŸãã
import tensorflow as tf
import tensorflow_text
import tensorflow_hub as hub
from tqdm import tqdm
tqdm.pandas()
embedder = hub.load(
"https://tfhub.dev/google/universal-sentence-encoder-multilingual/3")
features = np.stack(
df["title"].fillna("").progress_apply(lambda x: embedder(x).numpy().reshape(-1)).values
)
features
# => array([[ 0.09102602, 0.0063779 , -0.04502575, ..., -0.07357673, -0.01823404, 0.00417807],
# => [ 0.0847326 , 0.03644175, -0.03105138, ..., -0.06141949, -0.03723054, -0.04122324],
# => [ 0.02749815, -0.00558675, -0.04187826, ..., 0.04859524, 0.03867997, -0.07246225],
# => ...,
# => [-0.03633422, -0.03659198, -0.07171568, ..., 0.05222084, 0.04104229, -0.0548655 ],
# => [ 0.01590004, -0.03160512, -0.01822312, ..., -0.02742272, -0.00633312, -0.04813081],
# => [ 0.08572994, 0.01084783, 0.03214317, ..., 0.06420612, 0.00978137, 0.00233417]], dtype=float32)
features.shape
# => (12026, 512)
æç« ãã¯ãã«å士ã®é¡äŒŒåºŠã«ããç¹åŸŽ
æç« ãå«ãŸããã«ã©ã ãè€æ°ããå Žåã«ããããã«ã€ããŠãããããã¯ãã«åãæœããåŸããã®ãã¯ãã«å士ã®é¡äŒŒåºŠã枬ã£ãŠãã®ã¹ã³ã¢ãç¹åŸŽéãšããå ŽåããããŸãããã®ç¹åŸŽãäœãæ°æã¡ãšããŠã¯ãäŸãã° titleãš long_titleãšããã«ã©ã ããã£ããšããŠããã®2ã€ãã³ãããããŠãããã©ãããå€å®ãããªã©ã«äœ¿ããŸãã
ããã§ã¯ãåã®Universal Sentence Encoderã§äœæãããã¯ãã«ã䜿ã£ãŠ titleãšlong_titleã®éã®é¡äŒŒåºŠã枬ã£ãŠã¿ãŸãããã
long_features = np.stack(
df["long_title"].fillna("").progress_apply(lambda x: embedder(x).numpy().reshape(-1)).values
)
cosineé¡äŒŒåºŠã¯
ãšããŠèšç®ãããŸãããããç¹åŸŽéã®è¡åã«å¯ŸããŠè¡ãå Žåã¯æ¬¡ã®ããã«ãªããŸãã
(features * long_features).sum(axis=1) / (
np.linalg.norm(features, axis=1) * np.linalg.norm(long_features, axis=1))
# => array([0.41377544, 0.25844374, 0.48187917, ..., 0.8305303 , 0.7043016, 0.39323133], dtype=float32)
Discussion
çŽ æµãªãŸãšãããããšãããããŸãïŒïŒ
BERTã¢ãã«ã«ã€ããŠè³ªåãªã®ã§ãããinput_tensorãšmask_tensorãå°åºããäžã§ã以äžã®å ¬åŒããã¥ã¡ã³ããèªããš
tokenizer(batch_sentences, max_length=30, padding="max_length", truncation=True, return_tensors="pt")ã§ãŸãšããããããã«æããã®ã§ãããäœãæå³ã¯ããããŸããïŒïŒã³ã¡ã³ãããããšãããããŸãã
ç¹ã«æ·±ãæå³ã¯ãªãããã®å°åºãç¥ããŸããã§ãããããããšãããããŸãïŒ
ã¡ãã£ãšåäœã確èªããŠèŠãããšæããŸã
ãã¡ãããããããšãããããŸãã
ããããããæè¿ã®ãªãªãŒã¹ã§åºãã®ãããããŸããïŒðââïžðââïž
ç§ãæ¬èšäºãèŠãªããå匷ããŠç¥ã£ãããšã§ããŠããŸã æªçã§ç¢ºèšŒããªãã§ãããããupdateãããã°å¹žãã§ã^^